Conversion XML vers JSON : conventions, pièges et exemples de code
Vous récupérez une réponse depuis un point de terminaison SOAP, un flux RSS ou un sitemap.xml, et c’est du XML. Votre pile est nativement JSON : JavaScript côté client, REST au milieu, un magasin de documents tout en bas. Il vous faut donc convertir le XML en JSON, et vous prenez un parseur en vous attendant à une seule ligne de code.
C’est souvent suffisant, jusqu’à ce que la sortie vous pose problème. Un tableau que vous attendiez se révèle être un simple objet. Un attribut id disparaît. Un code postal comme 01234 revient sous la forme du nombre 1234. Aucun de ces problèmes n’est un bug de votre parseur. Ce sont les conséquences du mappage de deux modèles de données qui ne s’alignent pas, et pour convertir le XML en JSON de manière fiable, il faut comprendre les conventions qui comblent l’écart.
Ce guide explique pourquoi ces conventions existent, présente quatre façons de réaliser la conversion (navigateur, JavaScript, Python, CLI), les règles @_ et #text que partagent toutes les grandes bibliothèques, les cinq pièges à l’origine de pertes de données silencieuses, et comment reconvertir le JSON en XML pour un aller-retour propre. Les exemples sont réels et exécutables : collez-les dans Node, Python ou un shell et ils produisent la sortie indiquée dans les commentaires.
Pourquoi XML vers JSON nécessite des conventions (pas un simple reformatage)
XML et JSON se ressemblent en surface (tous deux sont des arbres de données nommées et imbriquées), mais leurs modèles sous-jacents divergent sur des points qui comptent. Les éléments XML peuvent porter des attributs, contenir du contenu mixte (du texte entrelacé avec des éléments enfants) et appartenir à des espaces de noms. JSON n’a aucun de ces concepts. Il a des objets, des tableaux et quatre types scalaires. Convertir l’un en l’autre n’est pas un reformatage ; c’est une traduction entre deux grammaires dont l’une possède des notions que l’autre ne sait pas exprimer.
Avant de convertir quoi que ce soit, mieux vaut confirmer que la source est réellement valide. Un & non échappé ou une balise mal appariée sera rejeté par le parseur, donc passer l’entrée par un Formateur XML pour vérifier d’abord sa bonne formation vous épargne une série d’erreurs déroutantes.
Voici où les deux modèles s’écartent :
| Dimension | XML | JSON |
|---|---|---|
| Types de nœuds | éléments, attributs, texte, contenu mixte | objets, tableaux, chaîne, nombre, booléen, null |
| Contrainte de racine | exactement un élément racine requis | aucune contrainte de racine |
| Attributs | oui (id="P01") | aucun (nécessite une convention @_) |
| Éléments répétés | les frères de même nom sont légaux | les clés d’objet ne peuvent pas se répéter (nécessite une convention de tableau) |
| Système de types | le texte n’est pas typé — tout est une chaîne | types natifs |
| Espaces de noms | oui (xmlns) | aucun |
Parce que les modèles ne correspondent pas, chaque conversion XML vers JSON repose sur des conventions, et non sur un reformatage sans perte. Ces conventions ne sont pas arbitraires : fast-xml-parser (Node.js), xmltodict (Python) et JAXB (Java) ont tous convergé vers les deux mêmes marqueurs, @_ pour les attributs et #text pour le texte de contenu mixte. Une fois que vous les connaissez, ils se transposent d’un environnement d’exécution à l’autre. Les mêmes décalages de forme de données apparaissent dans d’autres conversions, comme les questions d’inférence de type du guide de conversion CSV vers JSON.
Comment convertir XML en JSON : 4 méthodes
Choisissez la méthode qui correspond à votre contexte : un collage ponctuel rapide, un service Node, un pipeline Python ou un script shell en CI.
Méthode 1 — Outil dans le navigateur (zéro configuration, confidentialité d’abord)
Pour une conversion ponctuelle, ou pour du XML que vous préféreriez ne pas coller sur un site web quelconque, un convertisseur dans le navigateur est le chemin le plus rapide. Collez du XML dans le Convertisseur XML vers JSON, et le JSON apparaît instantanément : aucune installation, aucun compte, aucun envoi. Tout s’exécute dans le moteur JavaScript de votre navigateur, donc les données ne quittent jamais la machine.
Ce dernier détail compte plus qu’il n’y paraît. Les enveloppes SOAP transportent des jetons WS-Security, les configurations internes transportent des chaînes de connexion, et les exports transportent des fiches clients. Comme rien n’est transmis, l’outil est sûr pour du XML contenant des identifiants ou des charges utiles sensibles. Vous pouvez le vérifier vous-même : ouvrez l’onglet Réseau et observez zéro requête se déclencher pendant que vous convertissez.
Méthode 2 — JavaScript / Node.js (fast-xml-parser)
Sous Node, fast-xml-parser est le choix standard. Les valeurs par défaut réservent toutefois des surprises (les attributs sont ignorés et les valeurs sont coercées), donc les options ci-dessous sont celles que vous voulez réellement pour une conversion fidèle :
// Convertir XML en JSON dans Node.js avec 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, // conserve les attributs (par défaut ils sont supprimés !)
attributeNamePrefix: '@_', // les attributs deviennent des clés préfixées par @_
textNodeName: '#text', // le texte de contenu mixte va sous #text
parseAttributeValue: false, // aucune coercition de type sur les attributs
parseTagValue: false, // aucune coercition de type sur le texte des éléments
});
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"
// }
// }
// }
// }
Les deux réglages le plus souvent oubliés sont ignoreAttributes: false et parseTagValue: false. Le premier conserve vos attributs id et currency ; le second empêche le parseur de transformer "79.99" en flottant et "01234" en 1234. La section des pièges revient sur les raisons pour lesquelles la préservation des chaînes est la valeur par défaut sûre.
Si vous voulez zéro dépendance dans le navigateur, le DOMParser natif effectue l’analyse pour vous, et vous parcourez le DOM vous-même :
// XML vers JSON sans dépendance dans le navigateur avec DOMParser
function xmlToJson(node) {
// Élément contenant uniquement du texte → valeur de type chaîne
const children = Array.from(node.children);
if (children.length === 0 && node.attributes.length === 0) {
return node.textContent.trim();
}
const obj = {};
// Attributs → préfixe @_
for (const attr of node.attributes) {
obj['@_' + attr.name] = attr.value;
}
// Élément avec attributs ET texte → #text
if (children.length === 0) {
obj['#text'] = node.textContent.trim();
return obj;
}
// Récursion dans les enfants, en regroupant les frères de même nom dans des tableaux
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 est conforme à XML 1.0, gère les CDATA et les références d’entités, et signale les erreurs de bonne formation, le tout sans installer de paquet. En contrepartie, vous êtes responsable de la logique de parcours, y compris la règle de regroupement en tableau montrée ci-dessus.
Méthode 3 — Python (xmltodict)
En Python, xmltodict réduit toute la tâche à un court pipeline. Il utilise @ comme préfixe d’attribut et #text pour le contenu mixte par défaut :
# Convertir XML en JSON en Python avec 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"
# }
# }
# }
# }
Par défaut, xmltodict conserve chaque valeur sous forme de chaîne, ce qui est le comportement que vous voulez. La seule option qu’il vaut la peine de connaître d’emblée est force_list, qui corrige le problème du tableau « un contre plusieurs » avant qu’il n’atteigne votre code :
# force_list garantit que <product> est toujours une liste, même s'il n'y en a qu'un
data = xmltodict.parse(xml, force_list={'product'})
products = data['catalog']['product'] # toujours une liste désormais
for p in products:
print(p['name'])
Sans force_list, un seul <product> produit un dict et deux produisent une liste, si bien que votre boucle plante dans le cas d’un seul élément. C’est le piège n° 1, abordé plus bas.
Méthode 4 — CLI (yq / one-liner Python)
Pour les scripts shell et les pipelines de CI, deux one-liners couvrent la plupart des cas. Le yq de Mike Farah lit du XML et émet du JSON directement :
# Avec yq (la version Go de Mike Farah)
yq -p=xml -o=json '.' input.xml
# Lecture depuis stdin
cat sitemap.xml | yq -p=xml -o=json '.'
Si xmltodict est déjà présent dans votre environnement, le one-liner Python ne nécessite aucun binaire supplémentaire :
python3 -c "import sys, xmltodict, json; print(json.dumps(xmltodict.parse(sys.stdin.read()), indent=2))" < input.xml
Tous deux lisent en flux depuis stdin, donc ils s’insèrent directement dans un pipeline, ce qui est utile pour convertir une réponse d’API au milieu d’un script ou normaliser un lot de fichiers dans une étape de build.
Les conventions d’attribut @_ et de texte #text expliquées
La plupart des pages de convertisseurs passent à côté de ce qui compte vraiment : ce que signifient ces curieuses clés @_ et #text et pourquoi elles existent. Une fois ces points clarifiés, la sortie cesse de paraître arbitraire.
Les attributs sont mappés vers des clés préfixées par @_. Un attribut n’a pas d’équivalent JSON : il n’existe pas, dans un objet, d’emplacement pour des « métadonnées sur cet objet » distinct d’un enfant. La convention consiste à donner aux attributs une clé préfixée par @_ :
<user id="42" role="admin"/>
→ { "user": { "@_id": "42", "@_role": "admin" } }
Pourquoi @_ en particulier ? Parce qu’aucun nom d’élément XML valide ne peut commencer par @, le préfixe ne peut jamais entrer en collision avec une clé réelle d’élément enfant. C’est en pratique un espace de noms réservé. (xmltodict utilise un @ nu ; fast-xml-parser utilise @_ par défaut. Le principe est identique.)
Le contenu mixte est mappé vers #text. Quand un élément possède à la fois un attribut et une valeur textuelle, le texte a besoin d’un endroit où loger aux côtés des clés d’attribut. C’est #text :
<price currency="USD">29.99</price>
→ { "price": { "@_currency": "USD", "#text": "29.99" } }
Les éléments en texte brut deviennent une valeur de type chaîne directe. Aucun attribut, aucun enfant, juste du texte : l’indirection #text est alors inutile. <name>Alice</name> devient "name": "Alice". La clé #text n’apparaît que lorsque des attributs forcent la valeur de l’élément à être un objet.
Cette asymétrie est la source d’un bug subtil. Le même nom d’élément peut produire une chaîne brute dans un document et un objet @_/#text dans un autre, selon que cette instance portait ou non un attribut. Un <price> sans attribut currency est la chaîne "29.99" ; le même <price currency="USD"> est { "@_currency": "USD", "#text": "29.99" }. Du code qui lit node.price directement fonctionne pour une forme et casse silencieusement sur l’autre. L’accesseur défensif consiste à vérifier le type : const amount = typeof node.price === 'object' ? node.price['#text'] : node.price;.
Les CDATA deviennent du contenu en texte brut. Une section <![CDATA[if (a < b) return;]]> n’est qu’un mécanisme d’échappement, donc les délimiteurs sont retirés et le texte intérieur est préservé : "if (a < b) return;". Rien de particulier ne survit jusque dans le JSON.
Une fois que vous avez une sortie, collez-la dans un Formateur JSON pour valider la sortie JSON et confirmer que la structure correspond à ce que votre consommateur attend avant de la brancher dans votre code.
5 pièges de la conversion XML vers JSON et comment les éviter
Ce sont les défaillances qui passent la revue de code et apparaissent en production. Chacune remonte au décalage de modèle évoqué au début de ce guide.
1. Ambiguïté de tableau (un contre plusieurs). Un seul <item> devient un objet ; deux ou plus deviennent un tableau. La forme du JSON dépend du nombre de frères présents dans ce document. Du code consommateur comme result.items.item.forEach(...) fonctionne en test, où votre fixture comporte trois éléments, et lève TypeError: not a function en production lorsqu’une fiche en a exactement un.
// Deux frères <book> → tableau
// <library><book>A</book><book>B</book></library>
// → { "library": { "book": ["A", "B"] } }
// Un seul <book> → objet, PAS un tableau
// <library><book>A</book></library>
// → { "library": { "book": "A" } }
// Normaliser pour que les deux cas se comportent de façon identique
const books = [].concat(result.library?.book ?? []);
books.forEach(b => console.log(b)); // sûr pour 0, 1 ou plusieurs
L’idiome [].concat(x ?? []) vaut la peine d’être mémorisé : une valeur absente devient [], un objet unique devient [object], et un tableau existant passe inchangé. En Python, passez force_list={'book'} à xmltodict.parse() et la valeur est toujours une liste, ce qui vous évite la normalisation.
2. Attributs supprimés en silence. Plusieurs bibliothèques ignorent les attributs par défaut ; fast-xml-parser fait exactement cela jusqu’à ce que vous définissiez ignoreAttributes: false. La conversion semble avoir réussi, le JSON s’analyse sans problème, et vos valeurs id, currency et status ont simplement disparu. Définissez toujours le drapeau explicitement plutôt que de faire confiance à la valeur par défaut.
3. Aplatissement des espaces de noms. Une déclaration xmlns devient une clé @_xmlns ordinaire, et le préfixe dans <soap:Body> ne survit que comme partie de la clé de type chaîne "soap:Body". La sémantique, c’est-à-dire le fait que deux préfixes puissent se lier au même URI, est perdue.
<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": "..."
}
}
Le préfixe soap: n’est désormais que du texte dans un nom de clé ; rien ne sait qu’il s’agit d’un espace de noms. Si deux éléments issus d’espaces de noms différents partagent un nom local, ils peuvent entrer en collision. Lorsque la gestion précise des espaces de noms fait partie des exigences, gardez les données dans un parseur conscient des espaces de noms et ne les aplatissez pas en JSON.
4. Aucune coercition de type, et c’est voulu. <zip>01234</zip> ne doit pas devenir 1234. Les codes de compte, codes postaux, identifiants complétés par des zéros et décimales sensibles à la précision se cassent tous sous une coercition silencieuse. Un bon convertisseur conserve tout sous forme de chaîne et vous laisse coercer délibérément :
// Ne pas se fier à la coercition implicite
if (config.timeout > 25) { /* fragile : "30" > 25 fonctionne par hasard */ }
// Coercer explicitement, uniquement là où vous connaissez le type
if (parseInt(config.timeout, 10) > 25) { /* sûr */ }
5. Avec perte : commentaires, instructions de traitement et ordre du contenu mixte. Les commentaires XML (<!-- ... -->) et les instructions de traitement (<?xml-stylesheet ?>) n’ont pas de place en JSON et sont écartés. L’ordre relatif du texte entrelacé avec des éléments enfants peut ne pas faire l’aller-retour. Si vous avez besoin de préserver chaque octet, pour réémettre le document source à l’identique, ne convertissez pas ; utilisez un Formateur XML pour reformater ou minifier sans toucher au modèle de données.
Reconvertir le JSON en XML (aller-retour)
Aller dans l’autre sens a sa propre subtilité, car JSON n’a aucune règle d’élément racine et XML en requiert exactement un. Le Convertisseur JSON vers XML qui l’accompagne applique les mêmes conventions @_/#text en sens inverse, de sorte qu’un trajet JSON → XML → JSON préserve les attributs, le texte et la structure.
La partie intéressante est la normalisation de la racine. Le convertisseur résout l’exigence de racine unique avec quatre règles :
- Objet à clé unique → cette clé devient la racine :
{ "config": {...} }→<config>...</config>. - Objet à plusieurs clés → enveloppé dans
<root>:{ "a": 1, "b": 2 }→<root><a>1</a><b>2</b></root>. - Tableau de premier niveau → enveloppé sous la forme
<root><item>...</item></root>, avec<item>comme nom de repli fixe. - Valeur primitive →
<root>value</root>.
Tout le reste reflète le sens direct. Les clés @_ deviennent des attributs, #text devient du contenu textuel, et un tableau JSON sous une clé produit des frères répétés de même nom ; le nom de clé est réutilisé, jamais mis au singulier :
// Convertir JSON en XML dans Node.js avec 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: '@_', // les clés @_ deviennent des attributs
textNodeName: '#text', // la clé #text devient du contenu textuel
ignoreAttributes: false, // traite les clés @_
format: true, // mise en forme lisible
});
console.log(builder.build(data));
// <catalog>
// <product id="P01">
// <name>Wireless Headphones</name>
// <price currency="USD">79.99</price>
// </product>
// </catalog>
Un détail que le builder gère pour vous : les caractères spéciaux dans le texte et les valeurs d’attribut (<, >, &, ") sont échappés vers leurs références d’entités, de sorte que la sortie reste bien formée.
FAQ
Comment les attributs XML sont-ils mappés en JSON ?
Les attributs deviennent des clés préfixées par @_, donc id="42" se transforme en "@_id": "42". C’est la convention partagée par fast-xml-parser et xmltodict, et le préfixe n’entre jamais en collision avec les noms d’éléments car aucun nom d’élément valide ne commence par @.
Pourquoi la conversion XML vers JSON garde-t-elle les nombres sous forme de chaînes ?
Parce que le convertisseur ne fait aucune coercition de type. Forcer 01234 en 1234 retirerait un zéro initial significatif des codes postaux, numéros de compte et identifiants complétés par des zéros. Conserver chaque valeur sous forme de chaîne est la valeur par défaut sûre ; coercez délibérément en aval, là où vous connaissez le type.
La conversion XML vers JSON est-elle sans perte ?
Non. Les commentaires et instructions de traitement sont écartés, la sémantique des espaces de noms n’est que partiellement préservée, et l’ordre du contenu mixte peut ne pas faire l’aller-retour. Quand vous avez besoin de préserver chaque octet, utilisez un Formateur XML pour reformater le XML au lieu de le convertir en JSON.
Comment les éléments XML répétés sont-ils gérés en JSON ?
Un seul enfant de même nom devient un objet ; deux ou plus deviennent un tableau. Comme la forme dépend du nombre de frères, votre code consommateur devrait toujours normaliser vers un tableau afin de gérer à la fois les cas à un seul et à plusieurs éléments sans planter.
Qu’advient-il des espaces de noms XML lors de la conversion en JSON ?
Une déclaration xmlns devient une clé @_xmlns ordinaire, et le préfixe reste à l’intérieur de la chaîne du nom d’élément, comme dans "soap:Body". La liaison sémantique d’un préfixe à un URI n’est pas interprétée, de sorte que des espaces de noms distincts peuvent s’aplatir ensemble.
Comment reconvertir le JSON en XML ?
Utilisez le Convertisseur JSON vers XML qui l’accompagne. Il applique les mêmes conventions @_ et #text en sens inverse, de sorte que les attributs, le contenu textuel et les tableaux se remappent symétriquement. C’est cette symétrie qui rend possible un aller-retour JSON → XML → JSON propre.
Puis-je convertir du XML avec plusieurs éléments racines ?
Non. Plusieurs éléments de premier niveau ne constituent pas du XML bien formé, donc le parseur rejette l’entrée. Enveloppez d’abord les fragments dans un seul élément racine (transformez <a/><b/> en <root><a/><b/></root>), puis convertissez.
Conclusion
La conversion XML vers JSON repose sur des conventions, et non sur un reformatage. Les règles sont cohérentes d’un environnement d’exécution à l’autre : les attributs sont mappés vers des clés @_, le texte de contenu mixte vers #text, les frères répétés vers des tableaux, et les valeurs restent des chaînes pour que les zéros initiaux et la précision survivent. Les pièges à retenir sont le glissement de forme « unique contre tableau », les attributs supprimés en silence et la perte des commentaires et de la sémantique des espaces de noms ; aucun n’est un bug, et tous sont prévisibles une fois que vous connaissez le décalage de modèle qui se cache derrière.
Lorsque vous avez besoin d’une conversion rapide et privée, collez dans le Convertisseur XML vers JSON : il s’exécute entièrement dans votre navigateur. Validez d’abord la source avec le Formateur XML, et allez dans l’autre sens avec le Convertisseur JSON vers XML lorsque vous avez besoin d’un XML aller-retour. Pour comprendre comment les modèles de formats de données façonnent le comportement de conversion, voyez les notes sur les différences entre YAML et JSON.