All articles
XMLJSONData formats

There is no correct way to convert XML to JSON

XML has attributes, ordered mixed content and no notion of arrays; JSON has none of those problems and none of those features. Four questions every XML-to-JSON converter has to answer, and the trade-offs behind each answer.

Written and maintained by Pura IA4 min read

Converting JSON to YAML is mechanical: the two share a data model, so a converter only has to re-serialize the same values. XML to JSON is different. XML can say things JSON has no place for — attributes, text that sits between child elements, the order of differently-named siblings — and JSON has a distinction XML never makes: whether something is a list. Any converter between them is quietly choosing a convention. Most do not tell you which.

Building the XML to JSON converter on this site meant answering four questions. None of them has a right answer, only answers that suit some uses better than others.

Question 1: where do attributes go?

An attribute and a child element both look like "a named value under this element", but they are not the same thing, and a converter that merges them cannot be reversed. The common fix is a prefix. This site uses @, and keeps the element’s text under a reserved #text key when there is anything else on the element:

Input
<book id="7" lang="en">Dune</book>
This site’s output
{
  "book": {
    "@id": "7",
    "@lang": "en",
    "#text": "Dune"
  }
}

Other published conventions make the same split with different symbols. BadgerFish, one of the oldest, also uses @ for attributes but $ for text; its own documentation’s example turns <alice charlie="david">bob</alice> into an object with "quot;: "bob" and "@charlie": "david". The Parker convention goes the other way and drops attributes entirely, which gives the cleanest JSON and loses data. Neither is wrong; they are tuned for different things — round-tripping versus reading.

Question 2: does a single element become a string or an object?

If every element became an object, <name>Dune</name> would turn into {"name": {"#text": "Dune"}} — faithful, and tiring to use. So most converters, including this one, collapse an element with no attributes and no children to its text. The cost shows up as soon as an attribute appears: the same field changes shape. A price with a currency attribute is an object; a price without one would be a string.

With an attribute, the value moves under #text
{
  "price": {
    "@currency": "BRL",
    "#text": "129.90"
  }
}

Question 3: when does a repeated element become an array?

This is the one that breaks production code. XML has no list type; a list is just the same element written several times. A converter that only sees one document has to guess, and the usual guess — the one this site makes — is "an array if it repeats here":

One <book>
{
  "shelf": {
    "book": "Dune"
  }
}
Two <book> elements
{
  "shelf": {
    "book": [
      "Dune",
      "Emma"
    ]
  }
}

Code written against the second shape — shelf.book.map(...) — throws the first time a shelf has exactly one book, because book is now a string. Nothing in the XML changed except the count. There are only three ways out, and each costs something:

  • Always produce arrays for element children. Predictable, and every single-valued field now needs [0] to read.
  • Use a schema (XSD, or a hand-written list of repeatable element names) to decide. Correct, but the converter is no longer general-purpose.
  • Normalize on the consuming side: wrap the value in an array if it is not one already. Cheap, and it has to be remembered at every call site.

A browser-based converter like this one cannot know your schema, so it uses the per-document guess and says so. If you are converting an API response you will consume in code, the third option is usually the practical one.

Question 4: what happens to mixed content?

Mixed content is text interleaved with elements — normal in documents, rare in data. A JSON object has no order between differently-named keys, so the text and the element can be kept, but not their positions relative to each other:

Input
<p>Hello <b>world</b>!</p>
This site’s output
{
  "p": {
    "#text": "Hello !",
    "b": "world"
  }
}
Converted back to XML
<?xml version="1.0" encoding="UTF-8"?>
<p>
  Hello !
  <b>world</b>
</p>

The exclamation mark has moved and the sentence no longer reads correctly. The only family of conventions that avoids this is the array-based one, such as JsonML, which represents every element as an array of tag name, attributes and children in order — ["p", "Hello ", ["b", "world"], "!"]. It round-trips any document perfectly and is awkward to query, because nothing is addressable by name. That is the right trade for markup and the wrong one for API payloads, which is why this site’s converter is built for the latter and documents the loss.

A fifth, smaller question: types

XML text is characters. 129.90 might be a price, a version number or a postcode, and parsing it as a number would also silently drop the trailing zero. This converter keeps every value a string, and carries that through to YAML, where it has to quote the value so it is not read back as a number:

The same price, as YAML
price:
  '@currency': BRL
  '#text': '129.90'

Choosing a convention

  • If the JSON will be converted back to XML, prefer a convention that keeps attributes (@ prefixes) and accept the extra keys.
  • If it will be read by code, decide the array question up front and normalize, instead of trusting whatever shape the first sample had.
  • If the XML is a document rather than data — paragraphs, inline formatting — use an order-preserving, array-based representation, or do not convert it at all.
  • Whatever you choose, write it down. Two services using two different converters on the same feed is a common source of "the field is sometimes an object" bugs.
The outputs labelled "this site’s output" are generated by the converter itself when this page is built, and the behaviour described for each is covered by tests. The BadgerFish, Parker and JsonML descriptions summarize those conventions’ own documentation and are not produced by code on this site.

Tools mentioned in this article

More articles