Skip to content

Instantly share code, notes, and snippets.

@doramatadora
Created August 21, 2023 11:47
Show Gist options
  • Save doramatadora/ed2d910cb868209beabaea44538f8b7b to your computer and use it in GitHub Desktop.
Save doramatadora/ed2d910cb868209beabaea44538f8b7b to your computer and use it in GitHub Desktop.
Convert JSON to XML with jq
# 💝 Use with jq 💝
# jq -r -f ./xml.jq some.json
# cat some.json | jq -r -f ./xml.jq
# Download or install jq from: https://jqlang.github.io/jq/download/
# Or: brew install jq
# Check out the manual: https://jqlang.github.io/jq/manual/
# To understand how content_key and attr_prefix work, run this script on the following JSON:
# https://gist.github.com/doramatadora/5623ea90e63ea1c6a15141e2fea054e8
def attr_prefix: "@";
def content_key: "#text" | "$t";
def escapes:
{
"&": "&",
"<": "&lt;",
">": "&gt;"
}
;
def attr_escapes:
{
"&": "&amp;",
"<": "&lt;",
"\"": "&quot;"
}
;
def text_to_xml:
split("") |
map(escapes[.] // .) |
join("")
;
def text_to_xml_attr_val:
split("") |
map(attr_escapes[.] // .) |
join("")
;
def node_to_xml:
if type == "string" then
text_to_xml
else
(
if .attrs then
.attrs |
to_entries |
map(" \(.key)=\"\(.value | text_to_xml_attr_val)\"") |
join("")
else
""
end
) as $attrs |
if .children and (.children | length) > 0 then
(
.children |
map(node_to_xml) |
join("")
) as $children |
"<\(.name)\($attrs)>\($children)</\(.name)>"
else
"<\(.name)\($attrs)>"
end
end
;
def fix_tree($name):
type as $type |
if $type == "array" then
.[] | fix_tree($name)
elif $type == "object" then
reduce to_entries[] as { key: $k, value: $v } (
{ name: $name, attrs: {}, children: [] };
if $k[0:1] == attr_prefix then
.attrs[$k[1:]] = $v
elif $k == content_key then
.children += [$v]
else
.children += [$v | fix_tree($k)]
end
)
else
{ name: $name, attrs: {}, children: [.] }
end
;
def fix_tree:
fix_tree("") |
.children[]
;
def leaves_to_string:
walk(
if type == "object" then
with_entries(.value |= leaves_to_string)
elif type == "array" then
map(leaves_to_string)
else
tostring
end
)
;
def to_xml:
leaves_to_string |
fix_tree |
node_to_xml
;
# Remove this line if you want to use this file as a library.
to_xml
@acidrain69
Copy link

I made some changes to add a root node and deal with empty objects. Not sure I did it right or optimally, I used chatgpt to assist

Note the new define and changes to node_to_xml and to_xml

# 💝 Use with jq 💝
# jq -r -f ./xml.jq some.json
# cat some.json | jq -r -f ./xml.jq

# Download or install jq from: https://jqlang.github.io/jq/download/
# Or: brew install jq
# Check out the manual: https://jqlang.github.io/jq/manual/

# To understand how content_key and attr_prefix work, run this script on the following JSON:
# https://gist.github.com/doramatadora/5623ea90e63ea1c6a15141e2fea054e8

def attr_prefix: "@";

def content_key: "#text" | "$t";

def root_element_name: "root";

def escapes:
   {
      "&": "&amp;",
      "<": "&lt;",
      ">": "&gt;"
   }
;

def attr_escapes:
   {
      "&": "&amp;",
      "<": "&lt;",
      "\"": "&quot;"
   }
;

def text_to_xml:
   split("") |
   map(escapes[.] // .) |
   join("")
;

def text_to_xml_attr_val:
   split("") |
   map(attr_escapes[.] // .) |
   join("")
;

def node_to_xml:
   if type == "string" then
      text_to_xml
   else
      (
         if .attrs then
            .attrs |
            to_entries |
            map(" \(.key)=\"\(.value | text_to_xml_attr_val)\"") |
            join("")
         else
            ""
         end
     ) as $attrs |

      if .children and (.children | length) > 0 then
         (
            .children |
            map(node_to_xml) |
            join("")
         ) as $children |
         "<\(.name)\($attrs)>\($children)</\(.name)>"
      elif .attrs | length == 0 and (.children | length == 0 or .children == null) then
         "<\(.name)\($attrs)/>"
      else
         "<\(.name)\($attrs)></\(.name)>"
      end
   end
;

def fix_tree($name):
   type as $type |
   if $type == "array" then
      .[] | fix_tree($name)
   elif $type == "object" then
      reduce to_entries[] as { key: $k, value: $v } (
         { name: $name, attrs: {}, children: [] };

         if $k[0:1] == attr_prefix then
            .attrs[$k[1:]] = $v
         elif $k == content_key then
            .children += [$v]
         else
            .children += [$v | fix_tree($k)]
         end
     )
   else
      { name: $name, attrs: {}, children: [.] }
   end
;

def fix_tree:
   fix_tree("") |
   .children[]
;

def leaves_to_string:
   walk(
      if type == "object" then
         with_entries(.value |= leaves_to_string)
      elif type == "array" then
         map(leaves_to_string)
      else
         tostring
      end
   )
;

def to_xml:
   leaves_to_string |
   fix_tree(root_element_name) |
   node_to_xml
;

# Remove this line if you want to use this file as a library.
to_xml

@acidrain69
Copy link

Actually I don't think it needs the extra case in node_to_xml, I think it just needs a / on line 67

"<\(.name)\($attrs)/>"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment