Converting XML to JSON: Handling Attributes, Repeated Elements, and Text Nodes (2026)
You need to consume a legacy SOAP/XML API in a modern JavaScript app that speaks JSON. You grab an XML-to-JSON converter. It works on the sample, then production data arrives: a single <item> sometimes, ten <item>s other timesâand your code crashes because sometimes it's an object, sometimes an array. Attributes vanish. Text mixed with child elements gets mangled.
XML and JSON have fundamentally different data models. Converting between them isn't lossless or unambiguousâyou have to make conventions explicit.
This guide covers the structural mismatches between XML and JSON and how real converters handle attributes, repeated elements, and mixed content.
Why XML â JSON Is Not 1:1
XML has concepts JSON lacks
| XML feature | JSON equivalent | Problem |
|---|---|---|
Attributes (<a href="x">) |
None | Where do attributes go? |
| Element order | Object keys unordered | Order can be lost |
| Repeated elements | Arrays | 1 element = object or array? |
| Mixed content (text + elements) | None | <p>Hi <b>there</b></p> |
Namespaces (ns:tag) |
None | Colon in keys |
| Comments / CDATA | None | Usually dropped |
Because JSON has no attributes and no ordering, every converter must invent conventions. Different libraries choose differentlyâwhich is why output varies.
Problem 1: Attributes
The XML
<book id="123" lang="en">
<title>Dune</title>
</book>
Common convention: prefix attributes with @
{
"book": {
"@id": "123",
"@lang": "en",
"title": "Dune"
}
}
The @ prefix distinguishes attributes from child elements. This is the most widely used convention (used by xml2js, fast-xml-parser, and the "Parker"/"BadgerFish" conventions).
Alternative: separate attributes object
{
"book": {
"$": { "id": "123", "lang": "en" },
"title": "Dune"
}
}
Takeaway: There's no standard. Check (or configure) how your converter represents attributes before writing code against the output.
Problem 2: The Single-vs-Array Trap (the #1 bug)
The problem
One item:
<cart><item>Apple</item></cart>
Naive conversion:
{ "cart": { "item": "Apple" } } â item is a STRING
Two items:
<cart><item>Apple</item><item>Pear</item></cart>
Naive conversion:
{ "cart": { "item": ["Apple", "Pear"] } } â item is an ARRAY
Why it crashes your code
// This works for two items, crashes for one:
data.cart.item.forEach(...) // TypeError when item is a string
data.cart.item.length // returns 5 (string length!) for one item
Your parser worked in testing (multiple items) and broke in production (single item), or vice versa.
The fix: force arrays for known-repeatable elements
Good converters let you declare which elements are always arrays:
// fast-xml-parser
import { XMLParser } from "fast-xml-parser";
const parser = new XMLParser({
isArray: (name) => name === "item" // item is ALWAYS an array
});
const result = parser.parse(xml);
// Now cart.item is always [...], even with one item
result.cart.item.forEach(...); // safe
Rule: For any element that can repeat, always coerce it to an array. Never let arity depend on the data.
Problem 3: Mixed Content (text + elements)
The XML
<p>Hello <b>world</b>, how are you?</p>
This mixes text nodes with child elements. JSON has no clean representation.
Common convention: #text key
{
"p": {
"#text": ["Hello ", ", how are you?"],
"b": "world"
}
}
But this loses ordering â you can't tell that "world" goes between the two text fragments.
Reality
Mixed content is where XMLâJSON is genuinely lossy. If your XML has significant mixed content (like XHTML), converting to JSON is the wrong approachâkeep it as XML or extract specific values.
Problem 4: Empty Elements and Nulls
Ambiguity
<value></value> <!-- empty string? -->
<value/> <!-- null? empty? -->
<value> </value> <!-- whitespace-only? -->
Different converters produce "", null, {}, or drop the key entirely.
Recommendation
Configure explicitly:
const parser = new XMLParser({
trimValues: true, // trim whitespace
parseTagValue: true, // "123" -> 123
});
And decide your null convention. Test with empty elements specifically.
Problem 5: Type Coercion
The XML
<config>
<port>8080</port>
<enabled>true</enabled>
<ratio>0.5</ratio>
<zip>01234</zip>
</config>
The trap
Aggressive parsers convert everything:
{ "port": 8080, "enabled": true, "ratio": 0.5, "zip": 1234 }
Notice zip: 01234 became 1234 â leading zero lost! Zip codes, phone numbers, and IDs are strings, not numbers.
The fix
const parser = new XMLParser({
parseTagValue: true,
numberParseOptions: { leadingZeros: false } // keep "01234" as string
});
Or disable auto-parsing and coerce types yourself where you know the schema.
Complete Example (fast-xml-parser)
import { XMLParser } from "fast-xml-parser";
const xml = \`
<library>
<book id="1" lang="en">
<title>Dune</title>
<author>Herbert</author>
</book>
<book id="2" lang="en">
<title>1984</title>
<author>Orwell</author>
</book>
</library>\`;
const parser = new XMLParser({
ignoreAttributes: false, // keep attributes
attributeNamePrefix: "@", // prefix them with @
isArray: (name) => name === "book", // book always array
trimValues: true
});
const result = parser.parse(xml);
console.log(result.library.book[0]["@id"]); // "1"
console.log(result.library.book[0].title); // "Dune"
result.library.book.forEach(b => console.log(b.title)); // safe: always array
When NOT to Convert XML to JSON
Converting is the wrong choice when:
- Significant mixed content (XHTML, documents with inline markup) â lossy
- Order matters and your JSON consumer needs it preserved
- Heavy namespace use â colons in keys get awkward
- You only need a few values â use XPath to extract them directly instead of converting the whole document
For pure data-exchange XML (config, records, API responses), conversion works well with the right conventions.
FAQ
Q: Why is my single element an object but multiple elements an array?
A: This is the classic XMLâJSON arity ambiguity. Configure your parser to always treat that element as an array (isArray in fast-xml-parser).
Q: Where do XML attributes go in JSON?
A: There's no standard. Most converters prefix them (@id) or nest them in a separate object ($). Check your library's convention.
Q: Why did my zip code 01234 become 1234?
A: Aggressive number parsing stripped the leading zero. Disable leading-zero parsing or treat IDs/codes as strings.
Q: Is XML to JSON lossless?
A: No. Attributes, element order, mixed content, comments, and namespaces have no clean JSON equivalent. It's lossy for document-style XML, acceptable for data-style XML.
Q: What's the best JavaScript library?
A: fast-xml-parser (fast, configurable) or xml2js (widely used). Both let you control attributes and array handling.
Conclusion
XML â JSON conversion breaks because the two formats have different data models. To convert reliably:
- Decide attribute handling (
@prefix is common) - Force arrays for repeatable elements (avoid the single-vs-array crash)
- Beware type coercion (leading zeros, IDs as strings)
- Accept mixed content is lossy (or keep it as XML)
- Configure explicitly â don't trust defaults across libraries
With the right parser configuration, data-style XML converts to clean, predictable JSON your modern app can consume safely.