Converting XML to JSON and the Attribute Problem Explained
XML has attributes, repeated children and mixed text, and JSON has objects and arrays. Where the mapping is lossy, what xmltodict does, and the fixes.

People ask for "XML to JSON" as though there were one answer. There is not. The two formats model data differently, and every converter has to make choices about the places where they do not line up. The choices are visible in the output, so a few minutes of looking at what a converter produced will save a day of debugging downstream code that assumed a shape.
The differences that cause trouble
XML has elements, attributes, text and order. JSON has objects (unordered key/value maps in the specification, though parsers keep insertion order), arrays, strings, numbers, booleans and null. A converter must answer four questions:
Where do attributes go? JSON objects have only keys, so an attribute and a child element with the same name would collide.
What about an element with both attributes and text? There is no place for the text.
How do you tell one child from a list of one? XML says nothing, since repetition is how it expresses a list.
What type is
29.50? XML says nothing, since it is all text.
Different converters answer differently, and the answers are the whole story.
What one converter does
The input:
<library updated="2026-10-01">
<book id="b1" available="true">
<title>Cache Patterns</title>
<author>Ana Lee</author>
<author>Bo Chen</author>
<price currency="USD">29.50</price>
<note>Covers R&D</note>
</book>
<book id="b2" available="false">
<title>SQL Tuning, 2nd Edition</title>
<author>Cy Ortiz</author>
<price currency="USD">44.00</price>
</book>
<book id="b3" available="true">
<title>Indexes "Explained"</title>
<author>Di Park</author>
<price currency="VND">250000</price>
<note/>
</book>
</library>import json, xmltodict
d = xmltodict.parse(open("library.xml", "rb").read())
print(json.dumps(d["library"]["book"][0], indent=1)){
"@id": "b1",
"@available": "true",
"title": "Cache Patterns",
"author": [
"Ana Lee",
"Bo Chen"
],
"price": {
"@currency": "USD",
"#text": "29.50"
},
"note": "Covers R&D"
}Read the answers off the output. Attributes got an @ prefix, so id became "@id", which is the convention most tools share (some use _id or -id), and it avoids a collision with a child called id. Text beside attributes goes under "#text", which is why price is an object here, with the currency attribute next to its value. A list appears for author, because it repeated. Types: "29.50" is a string, as is "true". The & in the note is decoded once, to R&D.
The shape changes with the data
The consequence is in the other two books. The type of each book's author value:
author type per book: ['list', 'str', 'str']The first book has two authors and gets a list, and the other two have one and get a string. The converter cannot know that author is meant to be a list, since the file does not say so, and a list of one is indistinguishable from a single element in XML. The same happens one level up: a library with a single book converted to book as an object, where a library with several gets an array.
single book -> dictThis is the most common bug in code that consumes converted XML. It works on the sample with three books and breaks on the customer whose feed has one. The fix is to tell the converter which elements are lists:
f = xmltodict.parse(open("library.xml", "rb").read(), force_list=("book", "author"))force_list -> ['list', 'list', 'list']With force_list, every author is a list, including the ones with a single name, and book is always an array. Build the set from the schema if you have one, or from a profile of the data if you do not (counting elements and attributes in an unknown XML file finds the repeated names). Treat the list as part of the contract between the producer and the consumer.
Empty, missing and attribute-bearing elements
The empty <note/> and the absent note differ in the output:
note values: ['Covers R&D', None, None]That print uses get, which hides the difference. In the real structure the first book has "note": "Covers R&D", the second has no note key at all, and the third has "note": null. So empty becomes null and absent stays absent, and a consumer that reads book["note"] gets a KeyError on the second book and None on the third. This is the same absent versus empty distinction as in handling optional XML elements and missing values, carried into a different format.
And the price shows the reverse problem. For the Vietnamese book:
price b3: {'@currency': 'VND', '#text': '250000'}Because price has an attribute, it is an object, and if a book had a price without a currency it would be a plain string. So the same field has two shapes: a string when there is no attribute, and an object when there is. Downstream code has to handle both, or the converter has to be told to always emit the object form. The cleaner fix is to design the target shape yourself and write the extraction in code: {"price": 29.5, "currency": "USD"} is a better JSON document than anything a generic converter produces, and it takes ten lines (converting XML into a pandas DataFrame shows the table version of the same idea).
Types
Every value in the output is a string. "available": "false" is a string, which is truthy in most languages, and "price": "29.50" will not sort numerically. A converter that guessed types would be wrong sometimes: a ZIP code 02134 would lose its zero, a part number 1E5 would become a float. xmltodict has a postprocessor hook where you can apply conversions per key, which is the right level: name the fields that are numbers and booleans, and convert only those. For money, convert to a decimal string or integer cents, not a binary float.
What cannot be represented
Two features of XML have no clean JSON equivalent.
Order of mixed children. In <p>Hello <b>big</b> world</p>, the text and element are interleaved. A dictionary of {"#text": ..., "b": ...} loses the order, and in some converters loses part of the text. If your XML is document-like, convert it with a representation that keeps a list of children in order (the "JsonML" style: ["p", "Hello ", ["b", "big"], " world"]), or do not convert at all (dealing with mixed content in XML text).
Interleaved repeated elements. <a/><b/><a/> becomes {"a": [..., ...], "b": ...}, and the original interleaving is gone. If order matters between different names, you need an ordered representation as well.
Comments, processing instructions, namespaces and CDATA. Converters drop most of these or keep namespace prefixes in the key names ("ns:title"), which makes consumers depend on the prefix the producer happened to choose.
A mapping you can explain
A conversion is a contract. When I do this for real, I write the mapping instead of using a generic converter, because the code is the documentation:
import json
from lxml import etree
root = etree.parse("library.xml").getroot()
out = [{"id": b.get("id"),
"available": b.get("available") == "true",
"title": b.findtext("title"),
"authors": [a.text for a in b.iterfind("author")],
"price": {"amount": b.findtext("price"), "currency": b.find("price").get("currency")},
"note": b.findtext("note") or None} for b in root.iterfind("book")]
print(json.dumps(out[0]))The shape of every book is the same, authors are always a list, the flag is a boolean, and the price keeps its currency in a named field. A reader of the JSON does not need to know the source was XML. For a quick inspection or an ad hoc job, a generic converter is fine. For a feed that will be read by other people's code, the explicit mapping is worth the ten minutes.
Going the other way
If you need the reverse, the same problems apply backwards: a name that is legal in JSON may be illegal in XML, null has no direct form, and an array of one does not survive. That direction is covered in converting JSON to XML with sensible element names, and the JSON side of the pair in converting JSON to CSV, XML and YAML. For the overall decision between formats, see XML versus JSON for data exchange. The conversion library is documented in the xmltodict repository, and the JSON format itself in RFC 8259. To check an XPath before you build the mapping, use the XPath tester.
When a round trip has to be exact
You will convert back and must get the same XML. Generic mappings lose order, comments, namespace prefixes, the difference between <a/> and <a></a>, and attribute order. If byte-equivalence matters, keep the original file and treat the JSON as a read-only view.
The consumer is a schema-driven tool. If something validates the JSON against a JSON Schema built for a specific shape, produce that shape directly with an explicit mapping.
The document is mixed content. Paragraphs with inline markup need an ordered, tree-shaped representation, not a dictionary.
The data is large. A converter that builds the whole dictionary needs memory many times the file's size. Stream the records with iterparse and write each as a JSON line.


