XML zu JSON: Attribute, Wiederholung und die Falle des einelementigen Arrays
Veröffentlicht am 20.7.2026 · 13 Min. Lesezeit · Entwickler-Tools
Daniel Okonkwo — Front-end-Entwickler und Tech-Redakteur bei Allin
Web-Performance · Dateiformate
Anhand von 4 Quellen geprüft
XML kann nicht ausdrücken, dass ein Element eine Liste ist. Die Dokumente <items><item>a</item></items> und <items><item>a</item><item>b</item></items> unterscheiden sich nur darin, wie viele Kinder es gibt, ein Konverter, der eines von beiden liest, kann also nicht wissen, ob item ein wiederholbares Element ist, das gerade nur eine Instanz hat. Dieses Werkzeug geht den üblichen Weg: Ein Kind liefert {"items":{"item":"a"}}, eine Zeichenkette, zwei liefern {"items":{"item":["a","b"]}}, ein Array. Jeder Verbraucher, der items.item[0] schreibt, funktioniert bis zu dem Tag, an dem eine Liste ein Element hat, und liest dann den Buchstaben a — das erste Zeichen der Zeichenkette — statt des Eintrags. Dasselbe Dokument kann beide Formen zugleich erzeugen: <r><g><i>1</i><i>2</i></g><g><i>3</i></g></r> liefert g als Array aus zwei Objekten, das erste mit i als Array aus zwei, das zweite mit i als schlichter Zeichenkette. Vier Konventionen begegnen dem. Die Form in einem Schema deklarieren, dem einzigen Ort, an dem Kardinalität je festgehalten wird; dem Konverter eine ausdrückliche Liste von Pfaden geben, die immer Arrays sind; Attributnamen mit einem Präfix versehen, damit sie nicht mit Elementnamen kollidieren; und einen Schlüssel für den Text eines Elements reservieren, das auch Attribute trägt. Dieses Werkzeug macht die letzten beiden: Attribute werden zu @name, und #text hält den eigenen Text eines Elements — ob dieses Attribute trägt, Elementkinder hat oder beides. Gemischter Inhalt bleibt erhalten: <p>Hello <b>world</b>!</p> liefert {"p":{"#text":["Hello","!"],"b":"world"}}, ein Array-Eintrag je Textabschnitt. Was nichts festhält, ist die Verschränkung: Die Ausgabe sagt nicht, dass Hello vor <b> stand und das Ausrufezeichen danach. Abschnitte aus reinem Leerraum fallen weg, der Rest wird getrimmt — es sei denn, xml:space="preserve" gilt, dann bleiben die Abstände exakt erhalten: <code xml:space="preserve"> keep </code> liefert {"code":{"@xml:space":"preserve","#text":" keep "}}. Jeder Wert ist eine Zeichenkette: <n>42</n> wird zu "42".
Zwei Dokumente, die sich nur in der Zahl der Kinder unterscheiden, ergeben zwei verschiedene JSON-Formen, und ohne Schema kann kein Konverter sie auseinanderhalten. Dazu, was dieser wirklich mit Attributen, gemischtem Inhalt und Leerraum macht — und das Einzige, was er weiterhin nicht festhalten kann.
Der Mangel steckt im Format, nicht im Konverter
Schreibe eine Bestellung mit drei Positionen in XML, und du wiederholst das line-Element dreimal. Schreibe eine Bestellung mit einer Position, und du schreibst das Element einmal. Nichts im Dokument unterscheidet das von einem Element, das schlicht nicht wiederholbar ist — kein Pluralzeichen, keine Kardinalität, keine Klammer. Die XML-Spezifikation legt fest, wie ein wohlgeformtes Dokument aussieht, und sagt überhaupt nichts darüber, wie oft ein Kind auftreten darf; diese Frage gehört einem Schema, und ein Dokument muss keines haben.
Also rät der Konverter, und es gibt nur zwei Arten zu raten. Immer ein Array erzeugen, was jedes einwertige Element zu einem Array aus einem macht und den Lärm in der Ausgabe verdoppelt. Oder ein Array nur dann erzeugen, wenn man Wiederholung sieht, was fast jedes Werkzeug tut, dieses eingeschlossen. Gib ihm <items><item>a</item></items>, und er liefert {"items":{"item":"a"}}. Gib ihm <items><item>a</item><item>b</item></items>, und er liefert {"items":{"item":["a","b"]}}. Gib ihm <items></items>, und er liefert {"items":""} — eine leere Zeichenkette, kein leeres Array, denn ohne Kinder gilt das Element als Blatt und sein Textinhalt ist nichts.
Die Folge: Die Form deines JSON hängt von deinen Daten ab und kann sich innerhalb eines einzigen Dokuments ändern. Wandle <r><g><i>1</i><i>2</i></g><g><i>3</i></g></r> um, und du bekommst g als Array aus zwei Objekten: im ersten ist i ein Array aus zwei Zeichenketten, im zweiten ist i die Zeichenkette 3. Code, der g durchläuft und dann i indiziert, funktioniert beim ersten Element und liest beim zweiten stillschweigend das Zeichen 3 — kein Fehler, kein Absturz, nur der falsche Wert, der weiterwandert. Das ist mit Abstand die häufigste Art, wie eine laufende XML-Integration in Produktion bricht, und sie bricht an dem Tag, an dem die Daten kleiner werden, nicht größer.
Die vier bestehenden Konventionen und die zwei, die dieses Werkzeug nutzt
Die erste ist ein Schema. XML Schema ist der einzige Ort im ganzen Stapel, an dem Kardinalität festgehalten wird: Eine Elementdeklaration trägt minOccurs und maxOccurs, und ein maxOccurs größer als eins ist genau die Aussage, dass dieses Element eine Liste ist. Ein Konverter mit dem Schema in der Hand kann für ein Dokument mit nur einem Kind korrekt ein Array aus einem erzeugen. Ohne das Schema existiert diese Information nirgends in der Datei, und keine Findigkeit holt sie zurück.
Die zweite ist eine Liste von Immer-Array-Pfaden, die dem Konverter von jemandem gegeben wird, der die Daten kennt. Die meisten ernsthaften XML-Bibliotheken nehmen so eine: Du benennst order.lines.line, und es wird ein Array, ganz gleich wie viele. Es ist ein Schema im Kleinen, einmal von einem Menschen geschrieben, der die Antwort kennt, und es ist die pragmatische Lösung, wenn kein formales Schema existiert. Dieses Werkzeug bietet die Option nicht. Verarbeitest du seine Ausgabe im Code, ist die defensive Form, vor der Nutzung zu normalisieren: den Wert selbst zu einem Array machen, falls er keines ist, und erst dann zu indizieren.
Die dritte und vierte betreffen Schlüssel, und dieses Werkzeug nutzt beide. Attribute bekommen das Präfix @, also liefert <book id="1"><title>Dune</title></book> {"book":{"@id":"1","title":"Dune"}}, und ein Attribut kann nie mit einem gleichnamigen Kind kollidieren: <book title="A"><title>B</title></book> behält beide, als @title und title. Und #text trägt den eigenen Text eines Elements, sobald dieser Text das Objekt mit etwas anderem teilen muss, sei es mit Attributen oder mit Elementkindern: <book id="1" lang="en">Dune</book> liefert {"book":{"@id":"1","@lang":"en","#text":"Dune"}}. Ein Element ohne Attribute und ohne Elementkinder überspringt die Hülle ganz und wird direkt zu seinem Text — deshalb ist <title>Dune</title> die Zeichenkette Dune und kein Objekt mit einem Schlüssel.
Gemischter Inhalt landet in #text, die Reihenfolge nicht
Gemischter Inhalt ist ein Element, dessen Kinder eine Mischung aus Text und anderen Elementen sind — ein Absatz mit einem fetten Wort in der Mitte, eine Beschreibung mit einem Inline-Link, eine Rechtsklausel mit einem hervorgehobenen Begriff. XML unterstützt das und nutzt es ständig; dafür ist dokumentorientiertes XML im Wesentlichen da. JSON hat keinen natürlichen Platz dafür, denn ein Objektschlüssel kann das fette Wort aufnehmen, aber nirgends lässt sich festhalten, dass dieses Wort zwischen zwei Textstücken stand.
Dieser Konverter löst das Problem, indem er den Text in #text sammelt. Gib ihm <p>Hello <b>world</b>!</p>, und er liefert {"p":{"#text":["Hello","!"],"b":"world"}}: ein Eintrag je Textabschnitt, in Dokumentreihenfolge, und das Markup, das sie getrennt hat, steht unter eigenem Schlüssel neben dem Array. Ein einzelner Abschnitt bleibt eine schlichte Zeichenkette statt eines Arrays mit einem Element: <r>lead<a>1</a></r> ergibt "#text":"lead". Attribute ändern daran nichts: <p id="1">Hello <b>x</b> tail</p> liefert {"p":{"@id":"1","#text":["Hello","tail"],"b":"x"}}. Nichts wird mehr weggeworfen, ein ONIX-Datensatz, ein DocBook-Fragment oder eine RSS-Beschreibung mit Markup behalten also ihren Fließtext. Was die Form nicht sagen kann, ist, wo das Markup stand: Nichts in diesem Objekt hält fest, dass Hello vor <b> kam und das Ausrufezeichen danach, und vertauscht man die beiden Abschnitte in der Quelle, entsteht identisches JSON. Trägt die Reihenfolge Bedeutung — eine Korrekturfassung, eine Transkription, alles, worin das Inline-Element eine Stelle im Satz markiert —, dann behalte den Absatz als XML-Zeichenkette, statt ihn zu konvertieren.
Leerraum wird in zwei Schichten behandelt. Ein Abschnitt, der zwischen zwei Tags nur aus Leerzeichen oder Zeilenumbrüchen besteht, ist Layout und kein Inhalt und fällt deshalb weg — darum füllt ein hübsch formatiertes Dokument dein JSON nicht mit leeren Zeichenketten — und der verbleibende Text wird getrimmt, <code> indented line</code> kommt also als indented line zurück. XML hat ein Attribut, dessen ganzer Zweck es ist, genau das zu untersagen, und der Konverter befolgt es jetzt: <code xml:space="preserve"> keep </code> liefert {"code":{"@xml:space":"preserve","#text":" keep "}}, samt Leerzeichen. Es vererbt sich so, wie die Spezifikation es vorsieht: Ein preserve an einem Vorfahren schützt jeden Nachfahren, bis einer davon wieder xml:space="default" erklärt.
Reihenfolge, Namensräume und Typen: drei weitere Dinge, die nicht überleben
Die Dokumentreihenfolge zwischen verschiedenen Elementnamen geht verloren. <r><a>1</a><b>x</b><a>2</a></r> liefert a als Array aus 1 und 2 und b als x, was so weit korrekt ist — die beiden a-Elemente werden eingesammelt, obwohl sie nicht benachbart sind — aber dass b dazwischen stand, ist weg. In einem Objekt haben Schlüssel keine Reihenfolge, auf die sich ein Verbraucher verlassen dürfte, es gibt also keinen Ort, das festzuhalten. Für datensatzförmiges XML spielt das keine Rolle. Für alles, worin die Reihenfolge Bedeutung trägt — ein Prozessprotokoll, eine Änderungshistorie, eine verschachtelte Erzählung — spielt es eine sehr große, und die Umwandlung ist auf eine Weise verlustbehaftet, die kein Größenvergleich zeigt.
Namensräume werden als Text mitgeführt, nicht verstanden. Ein präfigiertes Kind behält sein Präfix im Schlüssel: <r xmlns:ns="http://example.com"><ns:a>1</ns:a></r> ergibt den Schlüssel ns:a, und die Deklaration selbst erscheint als Attribut @xmlns:ns. Ein Standard-Namensraum wird als @xmlns deklariert, und die Kinder verlieren danach jede Spur davon — ein Element a in einem Namensraum und ein Element a ohne Namensraum ergeben denselben Schlüssel. Werden zwei Vokabulare in einem Dokument gemischt und benutzen beide denselben lokalen Namen, entsteht eine Kollision, für die der Konverter blind ist. Das Präfix ist zudem eine Wahl des Autors, kein Teil der Identität des Elements: Dasselbe Dokument mit einem anderen Präfix neu serialisiert erzeugt für identische Daten andere JSON-Schlüssel.
Typen werden nicht geraten, und hier liegt das Werkzeug richtig. <a><n>42</n><f>1.0</f><z>007</z><t>true</t></a> liefert vier Zeichenketten, keine Zahl, keinen Gleitkommawert, keine mit Nullen aufgefüllte Zeichenkette und keinen Boolean. Ein XML-Dokument ohne Schema hat ebenfalls keine Typen — alles ist Zeichendaten — sie zu erfinden hieße also, Information zu erfinden. Praktische Folge: Du vergleichst mit "true" statt mit true und wandelst dort selbst um, wo du eine Zahl brauchst. Das ist der richtige Handel: Zeichenkette zu Zahl ist eine Entscheidung, die Kenntnis des Feldes verlangt, und der Konverter hat keine.
Was dieser Konverter tatsächlich ablehnt
Der darunterliegende Parser ist der des Browsers, und er ist auf die richtige Weise streng: ein einzelnes <br> wird abgelehnt, ein nicht geschlossenes Tag wird abgelehnt, zwei Wurzelelemente werden abgelehnt, ein doppeltes Attribut wird abgelehnt. Strenge ist genau der Sinn eines XML-Parsers statt eines HTML-Parsers, und all das ist korrektes Verhalten.
Das Scheitern zu erkennen ist schwerer, als es aussieht, denn ein Browser-Parser wirft keine Ausnahme. Er liefert ein Dokument zurück, in das er ein Element namens parsererror gepfropft hat, und erwartet, dass der Aufrufer danach sucht. Nur den Namen zu suchen ist die naheliegende und die falsche Umsetzung: Sie lehnt jedes gültige Dokument ab, das ein eigenes parsererror-Element trägt — genau das, was ein Build-Log oder ein Prüfbericht enthält. Die Engines sind sich nicht einmal einig, wo die Markierung landet: Firefox wurzelt das ganze Dokument in einer Markierung im eigenen Fehler-Namensraum, während Blink und WebKit mitten im Baum eine im XHTML-Namensraum einfügen und dein Wurzelelement stehen lassen. Also fragt das Werkzeug die Engine, statt zu raten: Einmal je Sitzung parst es etwas absichtlich Kaputtes, liest den Namensraum der zurückkommenden Markierung und schaut fortan nur noch dort nach. <log><parsererror>none</parsererror><n>1</n></log> wird umgewandelt und liefert {"log":{"parsererror":"none","n":"1"}}.
| XML | Zurückgegebenes JSON | Was das bedeutet |
|---|---|---|
| <items><item>a</item></items> | {"items":{"item":"a"}} — eine Zeichenkette | Ein Kind ist keine Liste; [0] liefert das erste Zeichen |
| <items><item>a</item><item>b</item></items> | {"items":{"item":["a","b"]}} — ein Array | Die Form der Ausgabe hängt von der Anzahl ab, nicht vom Vokabular |
| <items></items> oder <items/> | {"items":""} — eine leere Zeichenkette | Weder leeres Array noch null: Drei Zustände fallen in einen |
| <book id="1" lang="en">Dune</book> | {"book":{"@id":"1","@lang":"en","#text":"Dune"}} | Attribute bekommen @, der eigene Text bekommt #text, keiner verdeckt ein Kind |
| <p>Hello <b>world</b>!</p> | {"p":{"#text":["Hello","!"],"b":"world"}} — ein Eintrag je Textabschnitt | Der Fließtext überlebt, die Verschränkung nicht — nichts sagt, dass Hello vor <b> stand |
| <code xml:space="preserve"> keep </code> | {"code":{"@xml:space":"preserve","#text":" keep "}} | Die Anweisung wird befolgt und vererbt; ohne sie wird jeder Abschnitt getrimmt |
| <log><parsererror>none</parsererror><n>1</n></log> | {"log":{"parsererror":"none","n":"1"}} — umgewandelt, nicht abgelehnt | Die Fehlerprüfung fragt die Engine nach dem Namensraum ihrer Markierung, dein eigenes Element ist also sicher |
Häufige Fragen
- Wie schreibe ich Code, der eine Liste mit einem Element übersteht?
- Normalisiere vor dem Lesen. Überall, wo dein Code eine Liste erwartet, erzwinge zuerst den Wert: Ist er schon ein Array, behalte ihn, sonst hülle ihn ein, und behandle einen fehlenden oder leeren Wert als leeres Array. Drei Zeilen an der Grenze deines Parsers, angewandt auf jeden Pfad, von dem du weißt, dass er wiederholbar ist, und der Einelementfall existiert für den Rest des Programms nicht mehr. Es an der Grenze zu tun zählt mehr als der genaue Code — eine an jeder Verwendungsstelle verstreute Umwandlung wird irgendwann an einer vergessen, und das wird die sein, die zum Monatsende läuft. Verarbeitest du denselben Feed regelmäßig, schreibe die Liste der wiederholbaren Pfade als Konstante neben den Parser, damit der Nächste sieht, welche Form du erwartet hast.
- Warum nicht immer ein Array erzeugen?
- Weil es unlesbar ist, und Lesbarkeit ist der Hauptgrund, aus dem Menschen XML nach JSON wandeln. Hülle jedes Element ein, und ein Datensatz mit fünfzehn einwertigen Feldern wird zu fünfzehn Arrays aus einem, die alle von Hand ausgepackt werden müssen, bevor man sie ausgeben kann. Bibliotheken, die diese Strategie unterstützen, machen sie genau deshalb meist pfadweise optional. Es gibt auch einen feineren Preis: Ein Array aus einem behauptet, dass das Element wiederholbar sei, und ist es das nicht, hast du eine falsche Tatsache über das Vokabular festgehalten. Zwischen beiden Fehlern lügt ein Konverter, der aus den Daten rät, wenigstens nie über das Dokument, das er bekam — er sagt dir nur weniger, als du brauchtest.
- Was passiert mit dem Text um mein <b>-Tag herum?
- Er landet in #text, neben dem Kindelement statt um es herum. <p>Hello <b>world</b>!</p> liefert {"p":{"#text":["Hello","!"],"b":"world"}}: ein Eintrag je Abschnitt, in Dokumentreihenfolge, und eine schlichte Zeichenkette statt eines Arrays, wenn es nur einen Abschnitt gibt. Abschnitte aus reinem Leerraum fallen weg, sofern nicht xml:space="preserve" gilt. Was du nicht zurückbekommst, ist die Verschränkung: Das JSON kann dir nicht sagen, dass Hello vor dem fetten Wort stand und das Ausrufezeichen danach, und hat ein Absatz drei Abschnitte und drei Inline-Elemente, ist das Zusammensetzen des Satzes aus diesem Objekt Ratearbeit. Brauchst du nur den Fließtext, lies #text und füge die Abschnitte zusammen. Brauchst du den Satz genau so, wie er geschrieben wurde, konvertiere den Absatz gar nicht: Behalte ihn als XML-Zeichenkette, oder nimm einen Konverter für gemischten Inhalt, der die Kinder eines Elements als ein einziges geordnetes Array aus Text- und Elementknoten darstellt statt als Objektschlüssel.
- Behandelt der Konverter CDATA und Entities?
- Ja, beides, und korrekt. Ein CDATA-Abschnitt wird ausgepackt und sein Inhalt wird gewöhnlicher Text, also liefert <a><![CDATA[<not>markup</not>]]></a> die Zeichenkette <not>markup</not> — die spitzen Klammern überleben als Zeichen, und genau darum geht es bei CDATA. Benannte Entities werden aufgelöst, & und < kommen also als Kaufmanns-Und und Kleiner-Zeichen zurück, numerische Zeichenreferenzen ebenfalls: café liefert café. Kommentare und Verarbeitungsanweisungen, samt der XML-Deklaration selbst, fallen vollständig weg, da sie kein Elementinhalt sind. Nichts davon ist die eigene Leistung des Konverters — es ist der XML-Parser des Browsers, der tut, was die Spezifikation sagt — ein guter Grund, für alles jenseits eines festen, selbst kontrollierten Feeds einen echten Parser einem regulären Ausdruck vorzuziehen.
- Mein Dokument ist wohlgeformt, aber das Werkzeug nennt es ungültig. Und jetzt?
- Prüfe zwei Dinge, in dieser Reihenfolge. Erstens: Suche nach HTML-Gewohnheiten, die XML nicht erlaubt — ein blankes <br> oder <img> ohne schließenden Schrägstrich, ein nicht maskiertes Kaufmanns-Und in einer URL, ein doppeltes Attribut an einem Element, oder ein verirrtes Zeichen vor der XML-Deklaration, etwa eine per Kopieren und Einfügen eingeschleppte Byte Order Mark. Zweitens: Stelle sicher, dass es genau ein Wurzelelement gibt. Zwei Geschwister auf oberster Ebene sind ein Fragment, kein Dokument, und müssen umhüllt werden, bevor irgendetwas sie parst. Sind beide sauber und es wird weiter abgelehnt, schicke es durch einen XML-Validator, der auf eine Zeile zeigt, statt dir nur zu sagen, dass etwas nicht stimmt.
Artikel, die dich interessieren könnten
Alle Ratgeber →Ähnliche Tools
Das hier beschreibt, was diese Konverter heute tun — geprüft, indem sie ausgeführt wurden — und nicht, was ein Standard einem Konverter vorschreibt. CSV hat keinen normativen Standard: RFC 4180 ist Informational und beschreibt gängige Praxis, weshalb zwei korrekt wirkende Werkzeuge bei derselben Datei auseinanderlaufen können, ohne dass eines falsch liegt. Abflachen, Typerkennung und Array-Erkennung sind Konventionen, keine Regeln. Bevor du Daten konvertierst, die du nicht neu exportieren kannst, lauf zuerst über eine Kopie und vergleiche Zeilen- und Spaltenzahl an beiden Enden.
Quellen
- W3C — Extensible Markup Language (XML) 1.0, Fifth Edition — section 2.1 on the single root element of a well-formed document, section 2.10 on white space handling and the xml:space attribute, and section 3.2.2 on mixed content
- W3C — W3C XML Schema Definition Language (XSD) 1.1 Part 1: Structures — minOccurs and maxOccurs on a particle: the only place in the XML stack where the cardinality of a repeated element is written down
- W3C — Namespaces in XML 1.0, Third Edition — an element's identity is its namespace name plus its local name, and the prefix is only a document-local shorthand chosen by the author
- WHATWG — HTML Standard, DOMParser and parseFromString — how an XML parse failure is reported as a document containing a parsererror element rather than as a thrown exception
Hast du einen Fehler in diesem Artikel entdeckt?