XML-zu-JSON-Konverter

Wandelt nach JSON, jede Abbildungsentscheidung bei Ihnen.

Eingabe
Ausgabe
WartetFügen Sie ein Dokument ein. Die Prüfung läuft während der Eingabe.

Alles läuft in diesem Tab. Nichts, was Sie einfügen, wird hochgeladen, protokolliert oder irgendwohin gesendet. Öffnen Sie Ihr Netzwerkpanel und prüfen Sie es.

Fügen Sie oben XML ein, und das JSON erscheint während der Eingabe. Zuerst wird das Dokument auf Wohlgeformtheit geprüft, denn ein Konverter, der sich durch kaputtes Markup rät, erzeugt JSON, das still falsch ist statt offensichtlich falsch. Scheitert die Syntax, bekommen Sie Zeile, Spalte und Lösung statt eines halben Objekts.

Man kommt hierher, wenn ein Dienst, mit dem Sie sprechen, XML spricht und alles danach JSON: eine SOAP-Antwort, gegen die Sie in einem Test assertieren wollen, ein Lieferantenfeed, den Sie in ein Skript ziehen, eine ONIX-Datei, in die Sie hineinsehen wollen, bevor Sie den Importer schreiben. Nichts wird hochgeladen, was zählt, wenn die Envelope ein Bearer-Token trägt.

Anders ist hier, dass die Abbildung nicht versteckt wird. Es gibt keinen einzig richtigen Weg, XML in JSON zu überführen; jeder Konverter trifft ein halbes Dutzend Entscheidungen für Sie, und fast keiner sagt welche. Diese Seite benennt jede davon, zeigt Ihnen den Schalter und meldet in einem Notizfeld neben der Ausgabe, was die Umwandlung gekostet hat.

Warum es keine richtige Antwort gibt, nur eine gewählte

Das Datenmodell von XML ist strikt reicher als das von JSON. XML hat geordnete Kinder, Attribute, Text zwischen Elementen, Namensräume, Kommentare und CDATA. JSON hat ungeordnete Objekte, Arrays, Zeichenketten, Zahlen, Wahrheitswerte und null. Jede Abbildung vom Ersten aufs Zweite muss etwas verwerfen oder etwas erfinden.

Michael Kay hat es in seinem Balisage-Papier zur schemabewussten Umwandlung klar gesagt: Ein generischer Konverter „rät, welche Semantik das Objektmodell hinter dem lexikalischen XML hat, und er rät falsch“. An diesen sieben Stellen muss er raten:

  • Attribute oder Kindelemente. <user id="7"/> und <user><id>7</id></user> sind verschiedene Dokumente, die die meisten als dasselbe JSON haben wollen. Führt man sie zusammen, liefert <user id="7"><id>8</id></user> einen doppelten Schlüssel, den RFC 8259 als unvorhersehbar bezeichnet.
  • Ein Vorkommen oder viele. Nichts in <items><item>a</item></items> sagt, ob item sich wiederholen kann, also rät ein Konverter durch Zählen.
  • Gemischter Inhalt. <p>Etwas Text <b>ist wichtig</b>.</p> hat drei geordnete Kinder, und ein JSON-Objekt kann diese Reihenfolge nicht ausdrücken.
  • Reiner Leerraum-Text. Zeilenumbruch und Einrückung zwischen Elementen in formatiertem XML sind echte Textknoten; sie zu behalten ist treu, aber unbrauchbar.
  • Namensräume. Ein Präfix ist nicht der Name, die URI ist es, und JSON kennt überhaupt keine Namensräume.
  • Kommentare und Verarbeitungsanweisungen. Beides existiert in JSON nicht.
  • Leere Elemente. <e/> lässt sich plausibel auf null, "", {} oder einen leeren Textschlüssel abbilden, und <e/> und <e></e> sind dasselbe Dokument.

Die hier verwendete Konvention und die Schalter, die sie ändern

Standard ist die Attribut-Präfix-Konvention, die Stefan Goessner 2006 beschrieben hat und von der fast-xml-parser, xml2js und das AWS SDK Varianten benutzen. Attribute bekommen das Präfix @_, damit sie nicht mit einem gleichnamigen Kindelement kollidieren. Text, der sich ein Element mit Attributen oder Kindern teilt, landet unter #text. Ein einmal auftretendes Element ist ein Wert; ein zweimal auftretendes wird ein Array. Ein reines Textelement fällt zu einer Zeichenkette zusammen, <name>Alice</name> ist also "Alice".

Das Wurzelelement bleibt der äußerste Schlüssel, Kommentare entfallen, Leerraum wird gekürzt und Entity-Referenzen werden aufgelöst, ein als "A &amp; B" geschriebenes Attribut kommt also als "A & B" an. Die Bedienelemente über dem Editor setzen Attributpräfix, Textschlüssel, eine Liste „immer ein Array“ mit Elementnamen und drei Kästchen: Namensraum-Präfixe entfernen, Attribute verwerfen, Typen umwandeln. Alle drei sind aus.

<order id="00042">
  <total currency="GBP">19.90</total>
  <line sku="0071">Widget</line>
  <line sku="0072">Gasket</line>
  <note/>
</order>

{
  "order": {
    "@_id": "00042",
    "total": { "@_currency": "GBP", "#text": "19.90" },
    "line": [
      { "@_sku": "0071", "#text": "Widget" },
      { "@_sku": "0072", "#text": "Gasket" }
    ],
    "note": ""
  }
}
Standardeinstellungen, angewendet auf ein Dokument mit allen vier heiklen Fällen.

Warum die Typumwandlung standardmäßig aus ist

XML ohne Schema ist durch und durch Text. "123" in eine Zahl zu verwandeln ist bequem – genau bis zu dem Moment, in dem es einen Bezeichner zerstört, und Bezeichner sind das meiste, was durch XML-Integrationen fließt.

Wenn Sie die Umwandlung einschalten, ist sie bewusst eng gefasst. Ein Wert wird nur dann zur Zahl, wenn er einer strengen JSON-Zahlengrammatik entspricht und danach einen Hin- und Rückweg übersteht: Das geparste Ergebnis wird zurückserialisiert und Zeichen für Zeichen mit dem Original verglichen. Diese Prüfung verhindert die Fehler, mit denen andere Konverter ausgeliefert werden. Auch mit eingeschalteter Umwandlung bleiben diese Zeichenketten:

  • Führende Nullen. "00042" und "01730" scheitern schon an der Grammatik, denn eine JSON-Zahl darf nicht mit einer Null vor weiteren Ziffern beginnen. Postleitzahlen, Bankleitzahlen und Artikelnummern überleben.
  • Nachgestellte Nullen hinter dem Komma. "19.90" ergibt geparst 19.9, was als "19.9" zurückserialisiert wird, also bleibt die Zeichenkette. "1.10" wird nicht zu 1.1.
  • Ganze Zahlen, die ein double nicht hält. "9007199254740993" ergibt einen Wert, der auf 992 endet, der Rückweg scheitert, die Zeichenkette bleibt. Genau das zerstört anderswo neunzehnstellige Auftragsnummern.
  • Nicht kanonische Exponenten. "1e5" ergibt 100000, was nicht "1e5" ist, also bleibt es Text.
  • Alles, was nicht exakt true, false oder null ist. "TRUE", "yes" und "Y" bleiben Zeichenketten.

Was verloren geht, und die benannten Alternativen

Drei Dinge überleben nicht. Die Dokumentreihenfolge zwischen unterschiedlich benannten Geschwistern verschwindet: Wechseln sich <line> und <discount> ab, sagt das JSON nichts darüber. Gemischter Inhalt wird flachgeklopft: Textfragmente werden aneinandergehängt und die Elemente dazwischen wandern in eigene Schlüssel, <root>35<nested>34</nested>46</root> ergibt also den Textwert "3546". Kommentare werden verworfen.

Namensräume bleiben wörtlich erhalten statt aufgelöst zu werden, soap:Body wird also zum Schlüssel "soap:Body". Das Entfernen der Präfixe ergibt "Body", aber dann kollidieren zwei Elemente aus verschiedenen Namensräumen mit gleichem lokalen Namen auf einem Schlüssel, und JSON bietet keine dritte Möglichkeit.

Wenn diese Konvention nicht das ist, was Ihr Konsument erwartet, haben die Alternativen Namen. BadgerFish legt Text unter $, Attribute unter @name und trägt jeden Namensraum im Gültigkeitsbereich mit: besser umkehrbar, kaum lesbar. Parker verwirft Attribute und schluckt die Wurzel – die schlankeste und einseitigste Ausgabe von allen. JsonML schreibt jedes Element als [Name, Attribute, Kinder] und ist die einzige verbreitete Konvention, die gemischten Inhalt und Kindreihenfolge unversehrt lässt.

Dasselbe in Code

Dieselbe Umwandlung in den Sprachen, die XML tatsächlich verarbeiten. Jedes Beispiel schaltet die Entity-Auflösung ab, denn in mehreren dieser Stacks lädt die Standardeinstellung eine im DOCTYPE genannte URL – das ist die XXE-Schwachstelle. Jedes benennt außerdem die Abbildungsentscheidung, die die Bibliothek für Sie trifft.

import { XMLParser } from 'fast-xml-parser';

const parser = new XMLParser({
  ignoreAttributes: false,       // default is true: attributes are DROPPED
  attributeNamePrefix: '@_',
  textNodeName: '#text',
  trimValues: true,
  parseTagValue: false,          // keep values as strings
  parseAttributeValue: false,
  processEntities: false,        // do not expand DOCTYPE-declared entities
  // FXP cannot know whether a tag repeats, so tell it which ones are lists.
  isArray: (name) => ['line', 'item', 'entry'].includes(name),
});

const json = parser.parse(xmlSource);

// fast-xml-parser never fetches anything over the network, so XXE is not
// reachable. It does expand entities declared in an internal DTD unless you
// set processEntities: false, so leave that off for untrusted input and cap
// the input size before parsing.
import json
import xmltodict

# disable_entities=True is the default in current xmltodict and blocks the
# expat entity-expansion attacks. Pass it explicitly so a downgrade of the
# dependency cannot silently re-enable them.
doc = xmltodict.parse(
    xml_source,
    disable_entities=True,
    attr_prefix='@_',
    cdata_key='#text',
    force_list=('line', 'item', 'entry'),   # the singleton fix
)

print(json.dumps(doc, indent=2, ensure_ascii=False))

# xmltodict returns dicts in document order, but that ordering has no meaning
# once serialised: JSON objects are unordered. Values are always strings.
# There is no coercion, which is the right default.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import javax.xml.stream.XMLInputFactory;

XMLInputFactory input = XMLInputFactory.newFactory();
// Neither of these is off by default. Both must be, for untrusted XML.
input.setProperty(XMLInputFactory.SUPPORT_DTD, false);
input.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);

XmlMapper xml = new XmlMapper(new XmlFactory(input));
JsonNode tree = xml.readTree(xmlSource);

String json = new ObjectMapper()
    .writerWithDefaultPrettyPrinter()
    .writeValueAsString(tree);

// Jackson's XML module merges attributes in with child elements: there is no
// prefix, so <user id="7"><id>8</id></user> loses one of the two. If your
// documents put data on attributes, bind to a class annotated with
// @JacksonXmlProperty(isAttribute = true) instead of reading a tree.
using System.Xml;
using Newtonsoft.Json;

var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,
    MaxCharactersFromEntities = 1024 * 1024,
    MaxCharactersInDocument = 20L * 1024 * 1024,
};

using var reader = XmlReader.Create(new StringReader(xmlSource), settings);
var document = new XmlDocument { XmlResolver = null };
document.Load(reader);

// omitRootObject: false keeps the root element as the outer key.
string json = JsonConvert.SerializeXmlNode(
    document, Newtonsoft.Json.Formatting.Indented, omitRootObject: false);

// Json.NET prefixes attributes with "@" and uses "#text" for text, close to
// the convention on this page. It has the singleton problem and no isArray
// hook: the only fix is a json:Array="true" attribute in the source XML,
// which you usually do not control.
<?php
libxml_use_internal_errors(true);

// LIBXML_NONET blocks network access for any DTD the document references.
// LIBXML_NOENT is deliberately NOT passed: it would substitute entities.
$xml = simplexml_load_string($source, 'SimpleXMLElement', LIBXML_NONET);

if ($xml === false) {
    foreach (libxml_get_errors() as $e) {
        fprintf(STDERR, "line %d col %d: %s\n", $e->line, $e->column, trim($e->message));
    }
    exit(1);
}

echo json_encode($xml, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), "\n";

// Two things to know first. json_encode() puts attributes under an
// "@attributes" object, not a prefix. And when an element has both text and
// child elements, SimpleXML drops the text entirely: mixed content does not
// survive this route at all.
# yq v4 (Mike Farah) reads XML and writes JSON with no Python dependency.
yq -p=xml -o=json '.' document.xml

# Attributes are prefixed with + by default; match this page's convention:
yq -p=xml -o=json --xml-attribute-prefix='@_' '.' document.xml

# yq resolves nothing over the network. It also has the singleton problem and
# no per-tag array option, so a list of one comes back as a scalar. Normalise
# on the way into jq:
yq -p=xml -o=json '.' document.xml \
  | jq '.order.line |= (if type == "array" then . else [.] end)'

Jede Bibliothek oben trifft mindestens eine Abbildungsentscheidung stillschweigend. Jackson verschmilzt Attribute mit Kindern, SimpleXML verwirft den Text aus gemischtem Inhalt, Json.NET und yq lassen sich nicht sagen, welche Elemente Listen sind. Das ist die durchschlagende Mehrdeutigkeit, kein Fehler einer dieser Bibliotheken. Welche Sie auch wählen: Schreiben Sie einen Test, der eine Antwort mit einem Element und eine mit mehreren durch denselben Codepfad schickt.

Häufige Fragen

Wird mein XML an einen Server geschickt?

Nein. Scanner, Mapper und JSON-Serialisierer sind allesamt JavaScript, das in diesem Tab läuft, und es gibt kein Backend, das die Umwandlung erreichen könnte.

Öffnen Sie die Entwicklerwerkzeuge, wechseln Sie auf den Netzwerk-Tab, fügen Sie ein und schauen Sie zu: Die Assets dieser Seite laden einmal, danach folgt nichts. Das zählt hier mehr als bei den meisten Konvertern, denn das XML, das Leute umwandeln, ist meist eine Integrations-Nutzlast, samt WS-Security-Header oder API-Schlüssel.

Warum liefert ein <item> ein Objekt und zwei ein Array?

Weil XML ohne Schema keine Kardinalität kennt. Nichts im Dokument sagt, ob item sich wiederholen kann, also rät ein Konverter durch Zählen – womit die Form Ihres JSON von den Daten abhängt statt vom Vertrag. So bricht eine XML-Integration am häufigsten: Code, der gegen eine Testantwort mit drei Einträgen geschrieben wurde, ruft items.item.map() auf und funktioniert, bis ein Kunde mit genau einer Bestellung kommt.

Die Abhilfe ist das Feld „immer ein Array“ über dem Editor. Machen Sie es im Code genauso: fast-xml-parser hat isArray, xmltodict hat force_list, und xml2js setzt explicitArray genau deshalb standardmäßig auf true.

Wie behalte ich XML-Attribute bei der Umwandlung nach JSON?

Sie bleiben standardmäßig erhalten, unter Schlüsseln mit dem Präfix @_, aus <user id="7"/> wird also {"user": {"@_id": "7"}}. Das Präfix sorgt dafür, dass ein Attribut und ein gleichnamiges Kindelement einander nicht überschreiben können.

Sie können das Präfix ändern oder leeren, um Attribute unter die Kinder zu mischen. Die verschmolzene Ausgabe liest sich besser und verliert bei einer Namenskollision still einen Wert, wie in <user id="7"><id>8</id></user>. Um Attribute ganz zu verwerfen, gibt es ein Kästchen – das tut die Parker-Konvention: gut für einseitige Extraktion, falsch für alles, was Sie zurückwandeln.

Was passiert mit Namensräumen und Präfixen wie soap:?

Der präfigierte Name wird wörtlich verwendet, soap:Body wird also zum Schlüssel "soap:Body". Nichts wird aufgelöst, weil JSON keine Namensraum-URI tragen kann, und das Notizfeld nennt die Zahl der deklarierten Namensräume.

„Namensraum-Präfixe entfernen“ ergibt "Body", meist genau das, was man will, wenn man einen Wert aus einer SOAP-Antwort zieht. Das Risiko ist real: Enthält das Dokument sowohl soap:Header als auch wsse:Header, verschmilzt das Entfernen sie auf einen Schlüssel, und einer gewinnt.

Soll ich „Zahlen und Wahrheitswerte umwandeln“ einschalten?

Nur wenn Sie wissen, dass in Ihren Daten keine Bezeichner stecken. Die Umwandlung hier ist strenger als üblich: Sie verlangt eine strenge Zahlengrammatik und danach eine Rückweg-Prüfung, sodass "00042", "19.90", "1.10" und jede für ein double zu große Ganzzahl Zeichenketten bleiben, statt verstümmelt zu werden.

Was sie nicht abfangen kann, ist ein Feld, das in Ihrem Muster "1" enthält und nächsten Dienstag "N/A". Ein vernünftiger Mittelweg: ausgeschaltet lassen und die zwei, drei Felder, die Sie wirklich brauchen, im eigenen Code umwandeln – dort steht die Entscheidung dann schriftlich.

Was macht es mit Kommentaren, CDATA und leeren Elementen?

Kommentare und Verarbeitungsanweisungen entfallen. JSON hat keinen Platz dafür, und einen Schlüssel zu erfinden macht die Ausgabe für Inhalte, die per Definition keine Daten sind, nur schwerer verarbeitbar.

CDATA wird als Text behandelt: <![CDATA[a < b]]> und a &lt; b sind derselbe Inhalt, zweimal verschieden geschrieben. Ein leeres Element wird zur leeren Zeichenkette, <note/> und <note></note> ergeben beide "note": "". null wurde verworfen, weil es sich als „kein Wert bekannt“ liest statt als „vorhanden und leer“.

Verwandte Werkzeuge

Zum Weiterlesen