Toolmingo
Guides8 min read

How to Convert JSON to XML (With Code Examples)

Learn how to serialize JSON to XML in JavaScript, Python, Go, and PHP — handling arrays, nesting, attributes, and special characters correctly.

JSON is the lingua franca of REST APIs and modern tooling. XML remains mandatory for SOAP services, Android resources, Maven/Gradle builds, Office Open XML documents, RSS/Atom feeds, and countless legacy integrations. Knowing how to convert JSON to XML — without losing data or producing malformed output — is a practical skill.

This guide covers the structural differences, working code in JavaScript, Python, Go, and PHP, and the edge cases that trip people up most.

Why JSON-to-XML is non-trivial

JSON and XML have fundamentally different data models:

JSON concept XML challenge
Arrays ([1, 2, 3]) XML has no native array — needs a wrapper element and repeated children
Root element XML requires exactly one root element; JSON objects/arrays have none
Keys with spaces or special chars XML tag names cannot contain spaces, start with numbers, or use most punctuation
null XML has no null — usually omit the element or use xsi:nil="true"
Numbers & booleans Everything in XML is text — type info is lost unless you use XML Schema
Nested objects Become nested elements (usually works, but naming matters)

The common convention is:

  • JSON object keys → XML element names
  • JSON string/number/boolean values → element text content
  • JSON arrays → repeated sibling elements under a parent wrapper
  • null → omitted element (or <field xsi:nil="true"/>)

Quick example

JSON input:

{
  "person": {
    "name": "Ana",
    "age": 30,
    "hobbies": ["reading", "hiking"]
  }
}

XML output (typical convention):

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <person>
    <name>Ana</name>
    <age>30</age>
    <hobbies>reading</hobbies>
    <hobbies>hiking</hobbies>
  </person>
</root>

The array items each become a <hobbies> element — repeated siblings under the same parent.

JavaScript

Browser / Node.js — manual builder

No built-in JSON→XML serializer exists, so you build a recursive function:

function jsonToXml(obj, rootTag = "root", indent = "") {
  if (typeof obj !== "object" || obj === null) {
    return `${indent}<${rootTag}>${escapeXml(String(obj))}</${rootTag}>\n`;
  }

  if (Array.isArray(obj)) {
    return obj
      .map((item) => jsonToXml(item, rootTag, indent))
      .join("");
  }

  const children = Object.entries(obj)
    .map(([key, val]) => {
      const safeKey = sanitizeTag(key);
      if (Array.isArray(val)) {
        return val
          .map((item) => jsonToXml(item, safeKey, indent + "  "))
          .join("");
      }
      return jsonToXml(val, safeKey, indent + "  ");
    })
    .join("");

  return `${indent}<${rootTag}>\n${children}${indent}</${rootTag}>\n`;
}

function escapeXml(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
}

function sanitizeTag(key) {
  // XML tag names: start with letter/underscore, no spaces
  return key.replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^([^a-zA-Z_])/, "_$1");
}

// Usage
const data = {
  person: { name: "Ana", age: 30, hobbies: ["reading", "hiking"] },
};

const xml = `<?xml version="1.0" encoding="UTF-8"?>\n` + jsonToXml(data);
console.log(xml);

Node.js — xml2js library (also handles XML→JSON)

npm install xml2js
const { Builder } = require("xml2js");

const builder = new Builder({
  rootName: "root",
  renderOpts: { pretty: true, indent: "  " },
  xmldec: { version: "1.0", encoding: "UTF-8" },
});

const data = {
  person: { name: "Ana", age: 30, hobbies: ["reading", "hiking"] },
};

const xml = builder.buildObject(data);
console.log(xml);

xml2js uses $ for attributes and _ for text nodes when round-tripping, so the output matches what its parser expects.

Python

dicttoxml (simplest)

pip install dicttoxml
import json
import dicttoxml
from xml.dom.minidom import parseString

data = {
    "person": {
        "name": "Ana",
        "age": 30,
        "hobbies": ["reading", "hiking"],
    }
}

xml_bytes = dicttoxml.dicttoxml(data, custom_root="root", attr_type=False)
# Pretty-print
dom = parseString(xml_bytes)
print(dom.toprettyxml(indent="  "))

attr_type=False removes the type="str" / type="int" attributes that dicttoxml adds by default — cleaner output for most use cases.

xml.etree.ElementTree — standard library, no deps

import json
import xml.etree.ElementTree as ET
from xml.dom.minidom import parseString

def dict_to_xml(parent: ET.Element, data):
    if isinstance(data, dict):
        for key, val in data.items():
            child = ET.SubElement(parent, key)
            dict_to_xml(child, val)
    elif isinstance(data, list):
        for item in data:
            # Reuse the parent tag for each list item
            sibling = ET.SubElement(parent.getparent() if hasattr(parent, "getparent") else parent, parent.tag)
            dict_to_xml(sibling, item)
    elif data is None:
        pass  # omit null values
    else:
        parent.text = str(data)

def json_to_xml(obj: dict, root_tag: str = "root") -> str:
    root = ET.Element(root_tag)
    for key, val in obj.items():
        child = ET.SubElement(root, key)
        if isinstance(val, list):
            for item in val:
                dict_to_xml(child, item)
        else:
            dict_to_xml(child, val)
    raw = ET.tostring(root, encoding="unicode")
    return parseString(raw).toprettyxml(indent="  ")

data = {"person": {"name": "Ana", "age": 30, "hobbies": ["reading", "hiking"]}}
print(json_to_xml(data))

Go

Go's encoding/xml package uses struct tags for marshalling. For arbitrary JSON, build an xml.Element tree manually:

package main

import (
    "encoding/json"
    "encoding/xml"
    "fmt"
    "os"
    "strings"
)

// AnyXML represents a generic XML node
type AnyXML struct {
    XMLName  xml.Name
    Children []AnyXML `xml:",any"`
    Text     string   `xml:",chardata"`
}

func jsonValueToXML(tag string, val any) AnyXML {
    node := AnyXML{XMLName: xml.Name{Local: sanitize(tag)}}

    switch v := val.(type) {
    case map[string]any:
        for k, child := range v {
            node.Children = append(node.Children, jsonValueToXML(k, child))
        }
    case []any:
        // Repeated siblings — caller handles this
    case nil:
        // omit
    default:
        node.Text = fmt.Sprintf("%v", v)
    }
    return node
}

func buildRoot(data map[string]any, rootTag string) AnyXML {
    root := AnyXML{XMLName: xml.Name{Local: rootTag}}
    for k, v := range data {
        switch arr := v.(type) {
        case []any:
            for _, item := range arr {
                root.Children = append(root.Children, jsonValueToXML(k, item))
            }
        default:
            root.Children = append(root.Children, jsonValueToXML(k, v))
        }
    }
    return root
}

func sanitize(s string) string {
    // Simple sanitizer: replace non-alphanumeric with underscore
    var b strings.Builder
    for i, r := range s {
        if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' || (i > 0 && r >= '0' && r <= '9') || r == '-' || r == '.' {
            b.WriteRune(r)
        } else {
            b.WriteRune('_')
        }
    }
    return b.String()
}

func main() {
    input := `{"person":{"name":"Ana","age":30,"hobbies":["reading","hiking"]}}`

    var data map[string]any
    if err := json.Unmarshal([]byte(input), &data); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    root := buildRoot(data, "root")
    out, err := xml.MarshalIndent(root, "", "  ")
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    fmt.Printf("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n%s\n", out)
}

PHP

SimpleXMLElement — built-in, no extensions

<?php
function arrayToXml(array $data, SimpleXMLElement $xml): void {
    foreach ($data as $key => $value) {
        // Sanitize: XML tag names can't start with a digit or contain spaces
        $tag = preg_replace('/[^a-zA-Z0-9._\-]/', '_', (string)$key);
        if (preg_match('/^[^a-zA-Z_]/', $tag)) {
            $tag = '_' . $tag;
        }

        if (is_array($value) && array_is_list($value)) {
            // Indexed array — repeated siblings
            foreach ($value as $item) {
                if (is_array($item)) {
                    $child = $xml->addChild($tag);
                    arrayToXml($item, $child);
                } else {
                    $xml->addChild($tag, htmlspecialchars((string)$item));
                }
            }
        } elseif (is_array($value)) {
            $child = $xml->addChild($tag);
            arrayToXml($value, $child);
        } elseif ($value === null) {
            // Omit null values
        } else {
            $xml->addChild($tag, htmlspecialchars((string)$value));
        }
    }
}

$json = '{"person":{"name":"Ana","age":30,"hobbies":["reading","hiking"]}}';
$data = json_decode($json, true);

$xml = new SimpleXMLElement('<root/>');
arrayToXml($data, $xml);

// Pretty-print via DOM
$dom = dom_import_simplexml($xml)->ownerDocument;
$dom->formatOutput = true;
echo $dom->saveXML();

ext-xmlwriter — streaming, good for large outputs

<?php
function writeValue(XMLWriter $w, string $tag, mixed $value): void {
    if (is_array($value) && array_is_list($value)) {
        foreach ($value as $item) {
            $w->startElement($tag);
            if (is_array($item)) {
                foreach ($item as $k => $v) writeValue($w, $k, $v);
            } else {
                $w->text((string)$item);
            }
            $w->endElement();
        }
    } elseif (is_array($value)) {
        $w->startElement($tag);
        foreach ($value as $k => $v) writeValue($w, $k, $v);
        $w->endElement();
    } elseif ($value !== null) {
        $w->writeElement($tag, (string)$value);
    }
}

$data = json_decode($json, true);

$w = new XMLWriter();
$w->openMemory();
$w->setIndent(true);
$w->setIndentString("  ");
$w->startDocument("1.0", "UTF-8");
$w->startElement("root");
foreach ($data as $key => $val) writeValue($w, $key, $val);
$w->endElement();
$w->endDocument();
echo $w->outputMemory();

Quick reference

Scenario Recommended approach
Browser one-off Manual recursive builder (no deps)
Node.js project xml2js Builder
Python, zero deps xml.etree.ElementTree
Python, simplest dicttoxml
Go encoding/xml + custom struct
PHP, simple SimpleXMLElement
PHP, large data XMLWriter streaming
Online Use Toolko Data Converter → JSON → XML

6 common mistakes

1. Forgetting the root element. XML must have exactly one root. An array at the top level ([1, 2, 3]) has no root — wrap it: <items><item>1</item>…</items>.

2. Invalid tag names. Tag names can't start with a digit, contain spaces, or use most punctuation. Sanitize keys before using them as element names (order-id is fine; order id is not).

3. Not escaping text content. &, <, >, ", and ' must be escaped as &amp;, &lt;, &gt;, &quot;, &apos;. Most libraries handle this — but raw string concatenation does not.

4. Type information lost. XML has no boolean or number type natively. 30 and "30" look identical in XML. Use XML Schema (xsd:integer) or a type attribute if you need round-trip fidelity.

5. Null handling. JSON null has no direct XML equivalent. Decide upfront: omit the element, or use xsi:nil="true" (requires the xs namespace declaration). Be consistent.

6. Array of arrays. JSON supports nested arrays ([[1,2],[3,4]]). XML has no equivalent — flatten or use an extra wrapper element per inner array.

FAQ

Q: Is there a standard mapping from JSON to XML? No universally accepted standard exists. JSONx (IBM) and JXON are the closest, but neither is widely adopted. Most converters follow informal conventions.

Q: Can I preserve JSON types (number, boolean) in XML? Only if you add XML Schema annotations. Without a schema, everything is a string. For round-trip fidelity add xsi:type="xsd:integer" attributes, or keep a JSON Schema alongside the XML.

Q: Can I produce XML attributes instead of child elements? Yes — many libraries support an @attr or $attr convention on JSON keys. xml2js uses $ for attributes and _ for text content. This depends on the library.

Q: What if a JSON key is an invalid XML tag name? Sanitize it: replace spaces/special chars with underscores, prepend an underscore if the key starts with a digit. Document the mapping so you can reverse it.

Q: How do I handle large JSON files? Use a streaming approach. In JavaScript, pipe JSONStream into a custom XML writer. In PHP, use XMLWriter (shown above). In Go, use json.Decoder with Token() to stream parse.

Q: Does the online tool handle this automatically? Yes — use the Toolko Data Converter tool. Paste your JSON, select XML as output, and copy the result.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools