Skip to content
Terug naar blog
Tutorials

XML naar JSON: conventies, valkuilen en codevoorbeelden

Converteer XML correct naar JSON: hoe attributen, arrays en namespaces worden toegewezen, waarom waarden strings blijven, met code voor JavaScript, Python en de browser.

13 min leestijd

XML naar JSON omzetten: conventies, valkuilen en codevoorbeelden

Je haalt een respons op van een SOAP-endpoint, een RSS-feed of een sitemap.xml, en het is XML. Je stack draait volledig op JSON: JavaScript aan de voorkant, REST in het midden, een documentopslag onderaan. Dus je moet XML naar JSON omzetten, en je grijpt naar een parser in de verwachting dat het een one-liner wordt.

Meestal is het dat ook — totdat de uitvoer je verrast. Een array die je verwachtte blijkt een enkel object te zijn. Een id-attribuut verdwijnt. Een postcode als 01234 komt terug als het getal 1234. Geen van deze dingen zijn bugs in je parser. Ze zijn het gevolg van het toewijzen van twee datamodellen die niet op elkaar aansluiten, en de enige manier om XML betrouwbaar naar JSON om te zetten is door de conventies te begrijpen die de kloof overbruggen.

Deze gids behandelt waarom die conventies bestaan, vier manieren om de conversie uit te voeren (browser, JavaScript, Python, CLI), de @_- en #text-regels die elke grote bibliotheek deelt, de vijf valkuilen die stil dataverlies veroorzaken, en hoe je JSON weer terug naar XML omzet voor een schone heen-en-terugtrip. De voorbeelden zijn uitvoerbaar: plak ze in Node, Python of een shell en je krijgt de uitvoer die in de commentaren staat.

Waarom XML-naar-JSON conventies nodig heeft (niet zomaar een herformattering)

XML en JSON lijken op het eerste gezicht op elkaar — beide zijn bomen van benoemde, geneste data — maar hun onderliggende modellen verschillen op een paar punten die je opbreken. XML-elementen kunnen attributen dragen, gemengde inhoud bevatten (tekst afgewisseld met onderliggende elementen) en onder namespaces leven. JSON kent geen van die begrippen. Het heeft objecten, arrays en vier scalaire types. De één naar de ander omzetten is geen herformattering; het is vertalen tussen twee grammatica’s, waarbij de één woorden kent die de ander niet kan spellen.

Voordat je iets omzet, loont het om te bevestigen dat de bron daadwerkelijk geldig is. Een verdwaalde, niet-geëncodeerde & of een niet-overeenkomende tag wordt door de parser afgewezen, dus de invoer eerst door een XML Formatter halen om de well-formedness te controleren bespaart je een ronde verwarrende fouten.

Hier lopen de twee modellen uiteen:

DimensieXMLJSON
Node-typeselementen, attributen, tekst, gemengde inhoudobjecten, arrays, string, number, boolean, null
Root-beperkingprecies één root-element vereistgeen root-beperking
Attributenja (id="P01")geen (vereist een @_-conventie)
Herhaalde elementengelijknamige siblings zijn toegestaanobjectsleutels kunnen niet herhalen (vereist een array-conventie)
Typesysteemtekst is ongetypeerd — alles is een stringnative types
Namespacesja (xmlns)geen

Omdat de modellen niet op elkaar aansluiten, is elke XML-naar-JSON-conversie conventiegedreven, geen verliesvrije herformattering. De conventies zijn niet willekeurig: fast-xml-parser (Node.js), xmltodict (Python) en JAXB (Java) zijn allemaal op dezelfde twee markeringen uitgekomen — @_ voor attributen, #text voor tekst in gemengde inhoud. Ken je ze eenmaal, dan gelden ze in elke runtime. Dezelfde modelafwijking duikt trouwens ook in andere conversies op, zoals bij de vragen rond type-inferentie in de CSV naar JSON conversiegids.

Zo zet je XML naar JSON om: 4 methoden

Kies de methode die bij je context past: een snelle eenmalige plak-actie, een Node-service, een Python-pijplijn of een shellscript in CI.

Methode 1 — browsergebaseerde tool (geen installatie, privacy voorop)

Voor een eenmalige conversie, of voor XML die je liever niet in een willekeurige website plakt, is een in-browser converter de snelste route. Plak XML in de XML naar JSON Omzetter, en de JSON verschijnt direct — geen installatie, geen account, geen upload. Alles draait in de JavaScript-engine van je browser, dus de data verlaat de machine nooit.

Dat laatste detail weegt zwaarder dan het lijkt. SOAP-envelopes dragen WS-Security-tokens, interne configs dragen connection strings en exports bevatten klantgegevens. Omdat er niets de deur uit gaat, kun je er gerust XML met inloggegevens of gevoelige payloads in plakken. Controleer het zelf: open het Netwerk-tabblad en je ziet nul verzoeken afgaan terwijl je converteert.

Methode 2 — JavaScript / Node.js (fast-xml-parser)

In Node is fast-xml-parser de standaardkeuze. De standaardinstellingen zullen je echter verrassen — attributen worden genegeerd en waarden worden omgezet — dus de opties hieronder zijn degene die je werkelijk wilt voor een getrouwe conversie:

// Zet XML om naar JSON in Node.js met fast-xml-parser
import { XMLParser } from 'fast-xml-parser';

const xml = `<catalog>
  <product id="P01">
    <name>Wireless Headphones</name>
    <price currency="USD">79.99</price>
  </product>
</catalog>`;

const parser = new XMLParser({
  ignoreAttributes: false,    // behoud attributen (standaard worden ze weggelaten!)
  attributeNamePrefix: '@_',  // attributen worden @_-geprefixte sleutels
  textNodeName: '#text',      // tekst uit gemengde inhoud komt onder #text
  parseAttributeValue: false, // geen type-omzetting op attributen
  parseTagValue: false,       // geen type-omzetting op elementtekst
});

const result = parser.parse(xml);
console.log(JSON.stringify(result, null, 2));
// {
//   "catalog": {
//     "product": {
//       "@_id": "P01",
//       "name": "Wireless Headphones",
//       "price": {
//         "@_currency": "USD",
//         "#text": "79.99"
//       }
//     }
//   }
// }

De twee instellingen die mensen vergeten zijn ignoreAttributes: false en parseTagValue: false. De eerste behoudt je id- en currency-attributen; de tweede voorkomt dat de parser "79.99" in een float verandert en "01234" in 1234. We komen in de sectie over valkuilen terug op waarom het behoud van strings de veilige standaard is.

Wil je nul dependencies in de browser, dan doet de native DOMParser het parsen voor je, en loop je zelf door de DOM:

// XML naar JSON zonder dependencies in de browser met DOMParser
function xmlToJson(node) {
  // Element met alleen tekst → string-waarde
  const children = Array.from(node.children);
  if (children.length === 0 && node.attributes.length === 0) {
    return node.textContent.trim();
  }

  const obj = {};
  // Attributen → @_-prefix
  for (const attr of node.attributes) {
    obj['@_' + attr.name] = attr.value;
  }
  // Element met attributen ÉN tekst → #text
  if (children.length === 0) {
    obj['#text'] = node.textContent.trim();
    return obj;
  }
  // Recursief door children, gelijknamige siblings verzamelen in arrays
  for (const child of children) {
    const value = xmlToJson(child);
    if (obj[child.tagName] === undefined) {
      obj[child.tagName] = value;
    } else {
      if (!Array.isArray(obj[child.tagName])) obj[child.tagName] = [obj[child.tagName]];
      obj[child.tagName].push(value);
    }
  }
  return obj;
}

const doc = new DOMParser().parseFromString(
  '<catalog><product id="P01"><name>Wireless Headphones</name></product></catalog>',
  'text/xml'
);
const json = { [doc.documentElement.tagName]: xmlToJson(doc.documentElement) };
console.log(JSON.stringify(json, null, 2));
// { "catalog": { "product": { "@_id": "P01", "name": "Wireless Headphones" } } }

DOMParser voldoet aan XML 1.0, verwerkt CDATA en entiteitsverwijzingen, en rapporteert fouten in de well-formedness — allemaal zonder een pakket te installeren. De afweging is dat je zelf de eigenaar bent van de traverseerlogica, inclusief de array-verzamelregel die hierboven getoond wordt.

Methode 3 — Python (xmltodict)

In Python brengt xmltodict het hele karwei terug tot een korte pijplijn. Het gebruikt standaard @ als attribuutprefix en #text voor gemengde inhoud:

# Zet XML om naar JSON in Python met xmltodict
import json
import xmltodict

xml = """<catalog>
  <product id="P01">
    <name>Wireless Headphones</name>
    <price currency="USD">79.99</price>
  </product>
</catalog>"""

data = xmltodict.parse(xml)
print(json.dumps(data, indent=2))
# {
#   "catalog": {
#     "product": {
#       "@id": "P01",
#       "name": "Wireless Headphones",
#       "price": {
#         "@currency": "USD",
#         "#text": "79.99"
#       }
#     }
#   }
# }

Standaard houdt xmltodict elke waarde als string, en dat is het gedrag dat je wilt. De ene optie die het waard is om vooraf te kennen is force_list, die het probleem van één-versus-veel arrays oplost voordat het je code bereikt:

# force_list garandeert dat <product> altijd een lijst is, ook bij precies één
data = xmltodict.parse(xml, force_list={'product'})
products = data['catalog']['product']  # nu altijd een lijst
for p in products:
    print(p['name'])

Zonder force_list levert één <product> een dict op en twee een lijst — en je loop crasht op het geval met één item. Dat is valkuil #1, die we hieronder behandelen.

Methode 4 — CLI (yq / Python one-liner)

Voor shellscripts en CI-pijplijnen dekken twee one-liners de meeste gevallen. De yq van Mike Farah leest XML en geeft direct JSON uit:

# Met yq (de Go-versie van Mike Farah)
yq -p=xml -o=json '.' input.xml

# Pipe vanaf stdin
cat sitemap.xml | yq -p=xml -o=json '.'

Als xmltodict al in je omgeving zit, heeft de Python one-liner geen extra binary nodig:

python3 -c "import sys, xmltodict, json; print(json.dumps(xmltodict.parse(sys.stdin.read()), indent=2))" < input.xml

Beide streamen vanaf stdin, dus ze passen zo in een pijplijn — handig om een API-respons midden in een script om te zetten of een batch bestanden in een buildstap te normaliseren.

De @_-attribuut- en #text-conventies uitgelegd

De meeste converterpagina’s slaan precies het deel over dat er werkelijk toe doet: wat de gekke @_- en #text-sleutels betekenen en waarom ze bestaan. Krijg deze helder en de uitvoer ziet er niet langer willekeurig uit.

Attributen worden toegewezen aan @_-geprefixte sleutels. Een attribuut heeft geen JSON-equivalent — er is geen plek in een object voor “metadata over dit object” die los staat van een child. De conventie is om attributen een sleutel te geven met een @_-prefix:

<user id="42" role="admin"/>
→ { "user": { "@_id": "42", "@_role": "admin" } }

Waarom juist @_? Omdat geen geldige XML-elementnaam met @ mag beginnen, kan de prefix nooit botsen met de sleutel van een echt onderliggend element. Het is een gereserveerde namespace die in het volle zicht verstopt zit. (xmltodict gebruikt een kale @; fast-xml-parser gebruikt standaard @_. Het principe is identiek.)

Gemengde inhoud wordt toegewezen aan #text. Wanneer een element zowel een attribuut als een tekstwaarde heeft, moet de tekst ergens kunnen leven naast de attribuutsleutels. Dat is #text:

<price currency="USD">29.99</price>
→ { "price": { "@_currency": "USD", "#text": "29.99" } }

Platte-tekstelementen worden een directe string-waarde. Geen attributen, geen children, alleen tekst — dus de #text-omweg is niet nodig. <name>Alice</name> wordt "name": "Alice". De #text-sleutel verschijnt alleen wanneer attributen de elementwaarde tot een object dwingen.

Deze asymmetrie is de bron van een subtiele bug. Dezelfde elementnaam kan een platte string opleveren in het ene document en een @_/#text-object in het andere, afhankelijk van of die specifieke instantie een attribuut droeg. Een <price> zonder currency-attribuut is de string "29.99"; dezelfde <price currency="USD"> is { "@_currency": "USD", "#text": "29.99" }. Code die node.price direct uitleest werkt voor de ene vorm en breekt stilletjes op de andere. De defensieve accessor is om het type te controleren: const amount = typeof node.price === 'object' ? node.price['#text'] : node.price;.

CDATA wordt platte tekstinhoud. Een sectie <![CDATA[if (a < b) return;]]> is slechts een escape-mechanisme, dus de delimiters worden weggehaald en de binnentekst blijft behouden: "if (a < b) return;". Niets bijzonders overleeft tot in de JSON.

Zodra je uitvoer hebt, plak je die in een JSON Formatter om de JSON-uitvoer te valideren en te bevestigen dat de structuur overeenkomt met wat je consument verwacht, voordat je het in code verwerkt.

5 XML-naar-JSON-valkuilen en hoe je ze vermijdt

Dit zijn de fouten die langs de code review komen en in productie opduiken. Elke fout is terug te voeren op de modelafwijking uit het begin van deze gids.

1. Array-ambiguïteit (één versus veel). Een enkele <item> wordt een object; twee of meer worden een array. De JSON-vorm hangt af van hoeveel siblings er toevallig in dat specifieke document zaten. Consumentcode als result.items.item.forEach(...) werkt in tests — waar je fixture drie items heeft — en gooit TypeError: not a function in productie wanneer een record er precies één heeft.

// Twee <book>-siblings → array
// <library><book>A</book><book>B</book></library>
// → { "library": { "book": ["A", "B"] } }

// Eén <book> → object, GEEN array
// <library><book>A</book></library>
// → { "library": { "book": "A" } }

// Normaliseer zodat beide gevallen zich identiek gedragen
const books = [].concat(result.library?.book ?? []);
books.forEach(b => console.log(b)); // veilig voor 0, 1 of veel

Het idioom [].concat(x ?? []) is het onthouden waard: een ontbrekende waarde wordt [], een enkel object wordt [object], en een bestaande array gaat ongewijzigd door. In Python geef je force_list={'book'} mee aan xmltodict.parse() en is de waarde altijd een lijst, zodat je de normalisatie volledig overslaat.

2. Attributen stilletjes weggelaten. Verschillende bibliotheken negeren attributen standaard — fast-xml-parser doet precies dit totdat je ignoreAttributes: false instelt. De conversie lijkt te zijn geslaagd, de JSON wordt prima ingelezen, en je waarden voor id, currency en status zijn simpelweg weg. Zet de vlag altijd expliciet en vertrouw niet op de standaard.

3. Namespace-afvlakking. Een xmlns-declaratie wordt een gewone @_xmlns-sleutel, en de prefix in <soap:Body> blijft alleen behouden als deel van de string-sleutel "soap:Body". De semantiek — dat twee prefixes aan dezelfde URI kunnen binden — gaat verloren.

<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>...</soap:Body>
</soap:Envelope>
→ {
    "soap:Envelope": {
      "@_xmlns:soap": "http://schemas.xmlsoap.org/soap/envelope/",
      "soap:Body": "..."
    }
  }

De prefix soap: is nu slechts tekst in een sleutelnaam; niets weet dat het een namespace is. Als twee elementen uit verschillende namespaces een lokale naam delen, kunnen ze botsen. Wanneer precieze namespace-verwerking deel uitmaakt van de vereiste, houd de data dan in een namespace-bewuste parser en vlak het helemaal niet af tot JSON.

4. Geen type-omzetting — en dat is correct. <zip>01234</zip> mag geen 1234 worden. Accountcodes, postcodes, opgevulde identifiers en precisiegevoelige decimalen breken allemaal onder stille omzetting. Een goede converter houdt alles als string en laat je bewust omzetten:

// Vertrouw niet op impliciete omzetting
if (config.timeout > 25) { /* broos: "30" > 25 werkt toevallig */ }

// Zet expliciet om, alleen waar je het type kent
if (parseInt(config.timeout, 10) > 25) { /* veilig */ }

5. Verliesgevoelig: commentaren, processing instructions en de volgorde van gemengde inhoud. XML-commentaren (<!-- ... -->) en processing instructions (<?xml-stylesheet ?>) hebben geen plek in JSON en worden weggegooid. De relatieve volgorde van tekst afgewisseld met onderliggende elementen overleeft de heen-en-terugtrip mogelijk niet. Als je elke byte behouden moet houden — om het exacte bronbestand opnieuw uit te zenden — converteer dan helemaal niet; gebruik een XML Formatter om te herformatteren of te minificeren zonder het datamodel aan te raken.

JSON terug naar XML omzetten (heen en terug)

De andere richting op gaan heeft zijn eigen kronkel, want JSON kent geen root-elementregel en XML vereist er precies één. De bijbehorende JSON naar XML Omzetter past dezelfde @_/#text-conventies omgekeerd toe, zodat een trip JSON → XML → JSON attributen, tekst en structuur behoudt.

De root-normalisatie is hier het lastige deel. De converter lost de eis van één root op met vier regels:

  • Object met één sleutel → die sleutel wordt de root: { "config": {...} }<config>...</config>.
  • Object met meerdere sleutels → ingepakt in <root>: { "a": 1, "b": 2 }<root><a>1</a><b>2</b></root>.
  • Array op het hoogste niveau → ingepakt als <root><item>...</item></root>, met <item> als vaste terugvalnaam.
  • Primitieve waarde<root>value</root>.

Al het andere spiegelt de voorwaartse richting. @_-sleutels worden attributen, #text wordt tekstinhoud, en een JSON-array onder een sleutel produceert herhaalde gelijknamige siblings — de sleutelnaam wordt hergebruikt, nooit in enkelvoud gezet:

// Zet JSON om naar XML in Node.js met fast-xml-parser
import { XMLBuilder } from 'fast-xml-parser';

const data = {
  catalog: {
    product: {
      '@_id': 'P01',
      name: 'Wireless Headphones',
      price: { '@_currency': 'USD', '#text': '79.99' },
    },
  },
};

const builder = new XMLBuilder({
  attributeNamePrefix: '@_', // @_-sleutels worden attributen
  textNodeName: '#text',     // #text-sleutel wordt tekstinhoud
  ignoreAttributes: false,   // verwerk @_-sleutels
  format: true,              // pretty-print
});

console.log(builder.build(data));
// <catalog>
//   <product id="P01">
//     <name>Wireless Headphones</name>
//     <price currency="USD">79.99</price>
//   </product>
// </catalog>

Eén detail dat de builder voor je afhandelt: speciale tekens in tekst- en attribuutwaarden (<, >, &, ") worden geëncodeerd naar hun entiteitsverwijzingen, zodat de uitvoer well-formed blijft.

FAQ

Hoe worden XML-attributen aan JSON toegewezen?

Attributen worden sleutels met een @_-prefix, dus id="42" wordt "@_id": "42". Dit is de gedeelde conventie van fast-xml-parser en xmltodict, en de prefix botst nooit met elementnamen omdat geen geldige elementnaam met @ begint.

Waarom houdt XML naar JSON getallen als strings?

Omdat de converter geen type-omzetting doet. 01234 in 1234 forceren zou een betekenisvolle voorloopnul wegnemen bij postcodes, accountnummers en opgevulde IDs. Elke waarde als string houden is de veilige standaard; zet stroomafwaarts bewust om waar je het type kent.

Is XML-naar-JSON-conversie verliesvrij?

Nee. Commentaren en processing instructions worden weggegooid, namespace-semantiek blijft slechts deels behouden, en de volgorde van gemengde inhoud overleeft de heen-en-terugtrip mogelijk niet. Wanneer je elke byte behouden moet houden, gebruik dan een XML Formatter om de XML te herformatteren in plaats van die naar JSON om te zetten.

Hoe worden herhaalde XML-elementen in JSON verwerkt?

Een enkele gelijknamige child wordt een object; twee of meer worden een array. Omdat de vorm afhangt van het aantal siblings, moet je consumentcode altijd naar een array normaliseren zodat hij zowel het ene-item- als het vele-items-geval verwerkt zonder te crashen.

Wat gebeurt er met XML-namespaces bij de omzetting naar JSON?

Een xmlns-declaratie wordt een gewone @_xmlns-sleutel, en de prefix blijft in de elementnaam-string, zoals in "soap:Body". De semantische binding van een prefix aan een URI wordt niet geïnterpreteerd, dus verschillende namespaces kunnen samen afvlakken.

Hoe zet ik JSON weer terug naar XML?

Gebruik de bijbehorende JSON naar XML Omzetter. Die past dezelfde @_- en #text-conventies omgekeerd toe, zodat attributen, tekstinhoud en arrays symmetrisch terug worden toegewezen. Die symmetrie is wat een schone heen-en-terugtrip JSON → XML → JSON mogelijk maakt.

Kan ik XML met meerdere root-elementen converteren?

Nee. Meerdere elementen op het hoogste niveau zijn geen well-formed XML, dus de parser wijst de invoer af. Pak de fragmenten eerst in één root-element in — maak van <a/><b/> een <root><a/><b/></root> — en converteer dan.

Conclusie

XML-naar-JSON-conversie is conventiegedreven, geen herformattering. De regels zijn consistent over alle runtimes: attributen worden toegewezen aan @_-sleutels, tekst uit gemengde inhoud aan #text, herhaalde siblings aan arrays, en waarden blijven strings zodat voorloopnullen en precisie overleven. De vallen om te onthouden zijn de vormverschuiving van één-versus-array, stilletjes weggelaten attributen, en het verlies van commentaren en namespace-semantiek — geen daarvan zijn bugs, allemaal voorspelbaar zodra je de modelafwijking erachter kent.

Wanneer je een snelle, privé conversie nodig hebt, plak je in de XML naar JSON Omzetter — die draait volledig in je browser. Valideer eerst de bron met de XML Formatter, en ga de andere richting op met de JSON naar XML Omzetter wanneer je heen-en-terug-XML nodig hebt. Voor meer over hoe de modellen van dataformaten conversiegedrag bepalen, zie de aantekeningen over verschillen tussen YAML en JSON.

Gerelateerde artikelen

Alle artikelen bekijken