Converting between JSON and XML: how it works and what breaks
Converting JSON to XML or back looks like it should be mechanical. It mostly is — but the two formats model data differently, so every converter has to make judgment calls, and some information genuinely cannot survive the trip. Knowing the rules saves you from silent data corruption.
Try the examples as you read: JSON → XML andXML → JSON run side by side in your browser.
The fundamental mismatch
JSON is a tree of typed values: objects, arrays, strings, numbers, booleans, null. XML is a tree of elements that can carry attributes, text, comments, and child elements — all of it untyped text. Converting between them means bridging four gaps:
- XML has attributes; JSON has no equivalent concept.
- JSON has real arrays; XML expresses “many” by repeating elements.
- JSON has data types; in XML everything is a string.
- XML needs one root element; a JSON document is any single value.
JSON → XML: the easy direction
Object keys become element names and values become content:
{
"user": {
"name": "Ana",
"age": 31,
"tags": ["admin", "editor"]
}
}<user>
<name>Ana</name>
<age>31</age>
<tags>admin</tags>
<tags>editor</tags>
</user>Even in this direction there are decisions to make:
- Arrays: most converters repeat the element (as above); others wrap items in a container like
<tags><item>…</item></tags>. Know which convention your consumer expects. - Roots: a top-level array like
[1, 2]has no natural element name, so converters invent a wrapper such as<root>. - Key names that aren’t valid element names: JSON allows keys like
"first name"or"2fa", but XML element names can’t contain spaces or start with a digit. Converters must rename or encode them — and that mapping is lossy. - Special characters: string values containing
&or<must be escaped as entities — see theXML escaping guide.
XML → JSON: where it gets opinionated
The reverse direction is where converters disagree, because XML carries more structure than JSON can express.
Attributes need a convention
<user id="17" role="admin">Ana</user>JSON has nowhere native to put id and role, so converters adopt a marker convention — commonly an @ prefix for attributes and a special key for text content:
{ "user": { "@id": "17", "@role": "admin", "#text": "Ana" } }Different tools use different markers (@id vs _id vs anattributes object), which is why XML→JSON output from one library often won’t round-trip through another.
One element or many? The array ambiguity
This is the classic conversion bug. Repeated elements clearly form an array:
<roles><role>admin</role><role>editor</role></roles>{ "roles": { "role": ["admin", "editor"] } }But when the same document happens to contain only one <role>, a naive converter produces a plain value instead of a one-item array — and the consuming code that expected an array crashes, but only on single-item data. If you control the consumer, handle both shapes; if you control the conversion, force known-repeating elements to always be arrays.
Types are guesses
XML text is untyped, so a converter seeing <age>31</age> must decide: the number 31 or the string "31"? Auto-detection usually helps, but it famously mangles values like <zip>01234</zip>(leading zero lost as a number) or version strings like 1.10. Check how your converter treats numeric-looking strings, booleans (true vs"true"), and empty elements (null, "", or{}?).
Some XML simply doesn’t fit
Comments, processing instructions, CDATA boundaries, namespaces, and mixed content(text interleaved with child elements, as in any HTML-like paragraph) have no faithful JSON representation. Converters drop or approximate them. If your XML is document-like rather than record-like, converting it to JSON is usually the wrong move — seeJSON vs XML for when each format fits.
What survives a round trip
A round trip — XML → JSON → XML, or the reverse — is the clearest way to see what a converter throws away, and it is worth running once against a representative document before you build a pipeline on top of one. Start here:
<!-- original -->
<article lang="en">
<!-- editorial note -->
<title>Ada</title>
<body>See <em>this</em> page</body>
</article><!-- after XML → JSON → XML -->
<article lang="en">
<title>Ada</title>
<body>
<em>this</em>
</body>
</article>The comment is gone, and — more seriously — the mixed content in <body>has lost the words around the <em> and the order they appeared in. The document still parses, still validates as well-formed, and now means something different. This is the failure mode to fear: not a crash, but a quiet change nobody notices until a reader complains.
Record-shaped XML — elements containing either text or child elements, never both — round trips cleanly apart from comments and type spelling. Document-shaped XML does not. That distinction, rather than any property of a particular library, is what determines whether conversion is safe.
Doing this in code
For anything beyond a one-off, use a library with explicit options rather than whatever the first search result suggests — the defaults are where the surprises live:
- JavaScript / Node —
fast-xml-parser. SetisArrayfor elements that repeat, and decideparseTagValuedeliberately rather than leaving type coercion on by accident. - Python —
xmltodictfor the quick path,lxmlwhen you need namespaces and XPath.xmltodicthas the single-element-array behaviour described above;force_listis the fix. - Java — Jackson’s
XmlMapper, which shares its configuration model with the JSON mapper you are probably already using. - .NET —
JsonConvert.SerializeXmlNode, which is convention-driven and documents its markers clearly. - Command line —
yqhandles XML as well as YAML and is the fastest way to convert a file in a shell pipeline.
Whichever you pick, pin the attribute-marker convention explicitly in your configuration. Leaving it at the default is how a library upgrade silently renames every key in your output.
Practical rules of thumb
- Validate before converting — a converter fed malformed input produces garbage or errors. (Both Indentio converters validate first and point at the exact problem.)
- Don’t expect byte-perfect round trips; treat conversion as a one-way translation you verify.
- Test the single-item-array case explicitly when consuming XML→JSON output.
- Watch numeric-looking strings: ZIP codes, phone numbers, version numbers, IDs with leading zeros.
- For document-style XML (mixed content, namespaces), keep it as XML.
Try it
Paste a document into the JSON → XML converter or theXML → JSON converter and compare input and output side by side. Everything runs locally in your browser — nothing is uploaded.