Convertisseur XML vers JSON

Convertit en JSON, chaque décision de mappage sous contrôle.

Entrée
Sortie
En attenteCollez un document pour le vérifier. La validation se fait à la frappe.

Tout s'exécute dans cet onglet. Rien de ce que vous collez n'est envoyé, journalisé ni transmis où que ce soit. Ouvrez votre panneau réseau et vérifiez.

Collez du XML ci-dessus et le JSON apparaît à la frappe. Le document est d’abord contrôlé pour sa bonne formation, parce qu’un convertisseur qui devine son chemin à travers du balisage cassé produit du JSON silencieusement faux plutôt qu’ouvertement faux. Si la syntaxe échoue, vous obtenez la ligne, la colonne et la correction au lieu d’un demi-objet.

On vient ici quand un service auquel vous parlez parle XML et que tout ce qui est en aval parle JSON : une réponse SOAP sur laquelle vous voulez écrire une assertion dans un test, un flux fournisseur que vous tirez dans un script, un fichier ONIX à explorer avant d’écrire l’importateur. Rien n’est envoyé, ce qui compte quand l’enveloppe porte un jeton.

Ce qui change ici, c’est que la conversion n’est pas cachée. Il n’existe pas une bonne façon de transformer du XML en JSON ; chaque convertisseur prend une demi-douzaine de décisions à votre place, et presque aucun ne dit lesquelles. Cette page nomme chaque décision, vous montre l’interrupteur, et signale ce que la conversion a coûté dans un panneau de notes à côté de la sortie.

Pourquoi il n’y a pas de bonne réponse, seulement une réponse choisie

Le modèle de données de XML est strictement plus riche que celui de JSON. XML a des enfants ordonnés, des attributs, du texte entremêlé d’éléments, des espaces de noms, des commentaires et du CDATA. JSON a des objets non ordonnés, des tableaux, des chaînes, des nombres, des booléens et null. Toute fonction du premier vers le second doit jeter quelque chose ou inventer quelque chose.

Michael Kay l’a dit sans détour dans son article Balisage sur la conversion guidée par le schéma : un convertisseur générique « devine quelle est la sémantique du modèle objet derrière le XML lexical, et il devine mal ». Voici les sept endroits où il doit deviner :

  • Attributs contre éléments enfants. <user id="7"/> et <user><id>7</id></user> sont deux documents différents que la plupart des gens veulent voir comme un même JSON. Fusionnez-les et <user id="7"><id>8</id></user> produit une clé en double, ce que la RFC 8259 qualifie d’imprévisible.
  • Une occurrence ou plusieurs. Rien dans <items><item>a</item></items> ne dit si item peut se répéter, donc un convertisseur devine en comptant.
  • Contenu mixte. <p>Du texte <b>important</b>.</p> a trois enfants ordonnés, et un objet JSON ne peut pas exprimer cet ordre.
  • Texte uniquement composé d’espaces. Le saut de ligne et l’indentation entre éléments dans du XML mis en forme sont de vrais nœuds texte : les garder est fidèle mais inexploitable.
  • Espaces de noms. Un préfixe n’est pas le nom, l’URI l’est, et JSON n’a aucune notion d’espace de noms.
  • Commentaires et instructions de traitement. Ni les uns ni les autres n’existent en JSON.
  • Éléments vides. <e/> se traduit plausiblement par null, "", {} ou une clé de texte vide, et <e/> et <e></e> sont le même document.

La convention retenue ici, et les réglages qui la changent

Par défaut, c’est la convention de préfixe d’attribut décrite par Stefan Goessner en 2006, dont fast-xml-parser, xml2js et le SDK AWS utilisent des variantes. Les attributs prennent le préfixe @_ afin de ne pas entrer en collision avec un élément enfant du même nom. Le texte partageant un élément avec des attributs ou des enfants passe sous #text. Un élément apparaissant une fois est une valeur ; apparaissant deux fois, il devient un tableau. Un élément purement textuel se réduit à une chaîne, donc <name>Alice</name> donne "Alice".

L’élément racine reste la clé la plus externe, les commentaires sont supprimés, les espaces sont rognés et les références d’entités sont résolues : un attribut écrit "A &amp; B" arrive donc comme "A & B". Les contrôles au-dessus de l’éditeur règlent le préfixe d’attribut, la clé de texte, une liste de noms d’éléments « toujours un tableau », et trois cases : retirer les préfixes d’espace de noms, ignorer les attributs, convertir les types. Les trois sont décochées.

<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": ""
  }
}
Réglages par défaut, sur un document contenant les quatre cas délicats.

Pourquoi la conversion de types est désactivée par défaut

Du XML sans schéma, c’est du texte de bout en bout. Transformer "123" en nombre est commode jusqu’à l’instant précis où cela détruit un identifiant, et les identifiants forment l’essentiel de ce qui circule dans les intégrations XML.

Quand vous activez la conversion, elle est volontairement étroite. Une valeur devient un nombre seulement si elle correspond à une grammaire de nombre JSON stricte puis survit à un aller-retour : le résultat analysé est resérialisé et comparé à l’original, caractère par caractère. C’est ce contrôle qui évite les défauts livrés par d’autres convertisseurs. Même conversion activée, ceux-ci restent des chaînes :

  • Zéros en tête. "00042" et "01730" échouent d’emblée à la grammaire, car un nombre JSON ne peut pas commencer par un zéro suivi d’autres chiffres. Codes postaux, codes guichet et références produits survivent.
  • Zéros finaux après la virgule. "19.90" s’analyse en 19.9, qui se resérialise en "19.9", donc la chaîne est conservée. "1.10" ne devient pas 1.1.
  • Entiers qu’un double ne peut pas contenir. "9007199254740993" s’analyse en une valeur finissant par 992, l’aller-retour échoue, la chaîne est conservée. C’est ce qui corrompt ailleurs des identifiants de commande à dix-neuf chiffres.
  • Exposants non canoniques. "1e5" s’analyse en 100000, qui n’est pas "1e5", donc cela reste du texte.
  • Tout ce qui n’est pas exactement true, false ou null. "TRUE", "yes" et "Y" restent des chaînes.

Ce qui est perdu, et les alternatives qui portent un nom

Trois choses ne survivent pas. L’ordre du document entre frères de noms différents disparaît : si <line> et <discount> alternent, le JSON ne dit rien de la façon dont. Le contenu mixte est aplati : les fragments de texte sont concaténés et les éléments intercalés partent dans leurs propres clés, donc <root>35<nested>34</nested>46</root> donne une valeur textuelle "3546". Les commentaires sont jetés.

Les espaces de noms sont conservés tels quels plutôt que résolus, donc soap:Body devient la clé "soap:Body". Retirer les préfixes donne "Body", mais deux éléments d’espaces de noms différents partageant un nom local entrent alors en collision sur une seule clé, et JSON n’offre pas de troisième option.

Si cette convention n’est pas celle qu’attend votre consommateur, les alternatives ont des noms. BadgerFish met le texte sous $, les attributs sous @nom et transporte tous les espaces de noms en portée : meilleur aller-retour, quasi illisible. Parker supprime les attributs et absorbe la racine : la sortie la plus dépouillée et la plus à sens unique. JsonML écrit chaque élément comme [nom, attributs, enfants], la seule convention courante qui conserve intacts le contenu mixte et l’ordre des enfants.

Le faire en code

La même conversion dans les langages qui consomment réellement du XML. Chaque exemple désactive la résolution des entités, car le comportement par défaut de plusieurs de ces piles va chercher une URL nommée dans un DOCTYPE, ce qui est la faille XXE. Chacun nomme aussi la décision de conversion que la bibliothèque prend pour vous.

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)'

Chacune des bibliothèques ci-dessus prend au moins une décision de conversion en silence. Jackson fusionne les attributs avec les enfants, SimpleXML jette le texte du contenu mixte, Json.NET et yq ne peuvent pas savoir quels éléments sont des listes. C’est l’ambiguïté qui transparaît, pas un bug chez l’une d’elles. Quelle que soit celle que vous choisissez, écrivez un test qui fait passer une réponse à un élément et une réponse à plusieurs par le même chemin de code.

Questions fréquentes

Mon XML est-il envoyé à un serveur ?

Non. L’analyseur, le convertisseur et le sérialiseur JSON sont tous du JavaScript qui tourne dans cet onglet, et il n’existe pas de backend que la conversion pourrait atteindre.

Ouvrez vos outils de développement, passez sur l’onglet Réseau, puis collez et regardez : les ressources de cette page se chargent une fois et rien ne suit. Cela compte davantage ici que sur la plupart des convertisseurs, car le XML que les gens convertissent est en général une charge utile d’intégration, avec son en-tête WS-Security ou sa clé d’API.

Pourquoi un seul <item> me donne-t-il un objet et deux un tableau ?

Parce que XML n’a pas de cardinalité sans schéma. Rien dans le document ne dit si item peut se répéter, donc un convertisseur devine en comptant, ce qui fait dépendre la forme de votre JSON des données plutôt que du contrat. C’est la façon la plus courante dont une intégration XML casse : du code écrit contre une réponse de test à trois éléments appelle items.item.map() et fonctionne jusqu’à l’arrivée d’un client à une seule commande.

La parade est le champ « toujours un tableau » au-dessus de l’éditeur. Faites de même en code : fast-xml-parser a isArray, xmltodict a force_list, et xml2js met explicitArray à true par défaut exactement pour cette raison.

Comment conserver les attributs XML lors de la conversion en JSON ?

Ils sont conservés par défaut, sous des clés préfixées par @_, donc <user id="7"/> devient {"user": {"@_id": "7"}}. Le préfixe existe pour qu’un attribut et un élément enfant du même nom ne puissent pas s’écraser.

Vous pouvez changer le préfixe ou le vider pour fondre les attributs parmi les enfants. La sortie fusionnée se lit mieux et perd silencieusement une valeur en cas de collision de noms, comme dans <user id="7"><id>8</id></user>. Pour supprimer complètement les attributs, il y a une case, ce que fait la convention Parker : très bien pour une extraction à sens unique, faux pour tout ce que vous reconvertirez.

Qu’advient-il des espaces de noms et des préfixes comme soap: ?

Le nom préfixé est utilisé tel quel, donc soap:Body devient la clé "soap:Body". Rien n’est résolu, parce que JSON ne peut pas porter d’URI d’espace de noms, et le panneau de notes indique combien d’espaces de noms étaient déclarés.

Cocher « retirer les préfixes d’espace de noms » donne "Body", en général ce que vous voulez pour extraire une valeur d’une réponse SOAP. Le risque est réel : si le document contient à la fois soap:Header et wsse:Header, le retrait les fond sur une seule clé et l’un des deux l’emporte.

Faut-il activer « convertir nombres et booléens » ?

Seulement si vous savez que vos données ne contiennent pas d’identifiants. La conversion est ici plus stricte qu’ailleurs : elle exige une grammaire de nombre stricte puis un aller-retour, si bien que "00042", "19.90", "1.10" et tout entier trop grand pour un double restent des chaînes au lieu d’être abîmés.

Ce qu’elle ne peut pas couvrir, c’est un champ qui vaut "1" dans votre échantillon et "N/A" mardi prochain. Un juste milieu raisonnable : laissez l’option désactivée et convertissez dans votre propre code les deux ou trois champs dont vous avez réellement besoin, là où la décision est écrite noir sur blanc.

Que fait-il des commentaires, du CDATA et des éléments vides ?

Les commentaires et les instructions de traitement sont supprimés. JSON n’a nulle part où les mettre, et inventer une clé rend la sortie plus difficile à consommer pour un contenu qui, par définition, n’est pas de la donnée.

Le CDATA est traité comme du texte : <![CDATA[a < b]]> et a &lt; b sont le même contenu écrit de deux façons. Un élément vide devient la chaîne vide, donc <note/> et <note></note> donnent tous deux "note": "". null a été écarté parce qu’il se lit comme « valeur inconnue » plutôt que « présent et vide ».

Outils associés

Pour aller plus loin