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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
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 &, <, >, ", '. 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.