ENES
xmlEngineering Guide

Converting XML to JSON: Handling Attributes, Repeated Elements, and Text Nodes (2026)

AC
Alex Chen·Lead Systems Architect
Published on 2026-09-07·9 min read·Daily Toolbox Engineering

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:

  1. Decide attribute handling (@ prefix is common)
  2. Force arrays for repeatable elements (avoid the single-vs-array crash)
  3. Beware type coercion (leading zeros, IDs as strings)
  4. Accept mixed content is lossy (or keep it as XML)
  5. 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.

#xml#json#conversion#data-formats#parsing
AC
Written by Alex ChenLead Architect

Alex Chen is a distributed systems engineer and core maintainer at Daily Toolbox with over 10 years of experience in client-side web technologies, RFC standards compliance, and cryptographic protocols. He specializes in zero-knowledge client architectures and WebAssembly-accelerated algorithms.

Try the free tools mentioned above

⚡ Open XML / JSON Converter →