How do I convert XML to JSON?

Dark navy cover card with the title 'How do I convert XML to JSON?' in white and a green accent bar

To convert XML to JSON, parse the XML with a library that has a defined mapping, then serialize the result as JSON. In Python that's xmltodict.parse plus json.dumps; on the command line it's yq; in .NET it's JsonConvert.SerializeXmlNode. Before you trust the output, decide how attributes, text and repeated elements should look, because that's where conversions quietly go wrong.

Why there's no single way to convert XML to JSON

XML and JSON don't model data the same way, so every converter has to invent a mapping. XML has attributes, mixed text, namespaces, comments and repeated sibling tags. JSON has objects, arrays, strings, numbers, booleans and null.

The convention most tools follow goes back to Stefan Goessner's 2006 article on XML.com. Attributes become keys prefixed with @. Goessner also admitted that converting back to the original XML isn't always possible.

If you're not sure what's inside the file yet, start with what an XML file is and how to open one.

In the sample below, the first order has one item and the second has two:

<orders>
  <order id="1">
    <item sku="A">Pen</item>
  </order>
  <order id="2">
    <item sku="B">Ink</item>
    <item sku="C">Paper</item>
  </order>
</orders>

Option 1: Python with xmltodict

xmltodict turns XML into nested dictionaries, and json.dumps turns those into JSON. Its README says "The default value for attr_prefix is @ and the default value for cdata_key is #text."

import json, xmltodict

with open("orders.xml") as f:
    doc = xmltodict.parse(f.read())

print(json.dumps(doc, indent=4))

The first order comes out like this:

{"@id": "1", "item": {"@sku": "A", "#text": "Pen"}}

The second order's item is a list of two objects. Same tag, two shapes. Any code that loops over order["item"] will break on the first order.

The fix is force_list. The xmltodict README describes it as a way to "Force list values for specific elements," accepting a boolean, a tuple of element names or a callable:

doc = xmltodict.parse(xml, force_list=("order", "item"))

Now every order and every item is a list, even when there's only one. process_namespaces=True expands namespace prefixes into full URIs. disable_entities=True is documented as "Disable entity parsing for security," so keep it on for files you didn't create.

Option 2: The command line with yq

yq reads XML and writes YAML or JSON, which makes it handy in shell scripts and CI. Pick the output with -o:

yq -p=xml -o=json orders.xml

yq follows the same pattern as the others: "Consecutive xml nodes with the same name are assumed to be arrays." The docs also state the catch plainly: "In XML, if your array has a single item, then yq doesn't know its an array." Their fix wraps the value with |= ([] + .):

yq -p=xml -o=json '.orders.order[].item |= ([] + .)' orders.xml

yq uses its own attribute prefix and text key rather than @ and #text. You can change both with --xml-attribute-prefix and --xml-content-name, which is useful if other code expects the xmltodict style.

Option 3: .NET with Json.NET

Json.NET converts an XmlDocument with JsonConvert.SerializeXmlNode. Its conversion rules are spelled out:

  • "Attributes are prefixed with an @ and should be at the start of the object."
  • "Single child text nodes are a value directly against an element, otherwise they are accessed via #text."
  • "Multiple nodes with the same name at the same level are grouped together into an array."
  • "Empty elements are null."

The docs give the same warning as everyone else: a single <Role> becomes text on a Role property, but several become an array. Json.NET's fix lives in the XML itself. Declare the namespace http://james.newtonking.com/projects/json and add json:Array='true' to the element, and the force array example shows <role json:Array='true'>Admin</role> coming out as "role": [ "Admin" ].

Option 4: JavaScript in the browser

Browsers parse XML natively with DOMParser. You then walk the nodes and build the object yourself, or hand the string to a library. Pass "application/xml" or "text/xml" as the type.

const doc = new DOMParser().parseFromString(xml, "application/xml");
if (doc.querySelector("parsererror")) throw new Error("Invalid XML");

parseFromString doesn't throw on bad input. According to MDN, "the returned document will contain a <parsererror> node describing the nature of the parsing error." Skip the check and you'll convert the error message into JSON.

For a one-off file, the XML to JSON converter on OnlineViewer Dev does this in the page with the browser's DOMParser. The tool code makes no network requests with what you paste, which matters for internal payloads.

How converters map the same XML

This table shows how each tool above treats the parts of XML that have no JSON equivalent:

XML feature xmltodict yq Json.NET
Attribute id="1" "@id": "1" own prefix, configurable "@id": "1"
Text next to attributes "#text" own content key, configurable "#text"
Tag that appears once object or string object or string object or string
Repeated tag list array array
Force an array force_list |= ([] + .) json:Array='true'
Numbers strings strings strings

The last row surprises people. XML has no number type, and yq's docs say "All values are assumed to be strings when parsing XML." Convert the fields you need after parsing. If those fields are long IDs, keep them as strings: JSON.parse loses precision on large numbers.

How to check it worked

  1. Run the JSON through a validator. A converter bug or a hand edit can leave a stray comma, and JSON doesn't allow trailing commas.
  2. Find a tag that appears once in one record and several times in another. Check it's an array in both.
  3. Look at an element that has both attributes and text. The text should sit under the text key, not vanish.
  4. If you have a JSON Schema for the target, validate the output against it. A schema that says "type": "array" catches the single-item problem straight away.

What goes wrong

The single-item array. A JSON-java issue shows the round-trip version: a one-item array converted to XML and back comes out as an object. Force arrays for every tag that can repeat.

Namespaces in keys. Without namespace processing, keys keep their prefixes, like soap:Envelope. That's fine until another sender uses a different prefix for the same namespace.

Comments and whitespace disappear. Most converters drop them. Json.NET keeps them under keys like #comment and #whitespace if you need them.

My take

Always force arrays by name. Listing the tags in force_list or json:Array is extra work, but the alternative is code that passes every test and fails on the first record with one line item. The catch: you have to know the schema. For unknown XML, force everything to lists and simplify later.

FAQ

Is there one standard way to convert XML to JSON?

No. XML has attributes, text, namespaces and repeated elements, and JSON has none of those concepts built in. Each library picks its own mapping. Most put attributes under keys starting with @ and text under #text, but the details differ.

Why is my XML element sometimes an array and sometimes an object?

Converters only create an array when they see the same tag more than once. A single child becomes a plain value or object. Force the array with xmltodict's force_list, yq's |= ([] + .), or Json.NET's json:Array attribute.

Why are numbers in my converted JSON wrapped in quotes?

XML has no number type, so most converters keep every value as a string. yq's docs say all values are assumed to be strings when parsing XML. Convert the fields you need in your own code after parsing.

Can I convert the JSON back to the same XML?

Not always. Stefan Goessner, who proposed the common @ convention in 2006, noted that converting back to the original XML isn't always possible. Comments, whitespace and the single-item array problem are the usual losses.

Sources