Ce qui se perd en convertissant entre JSON, YAML, CSV et XML

En quoi JSON, YAML, CSV, XML et TOML diffèrent sur les commentaires, les types, l'imbrication, les dates et l'ordre des clés, et lesquels de ces détails chaque conversion entre eux jette discrètement.

Chaque conversion entre formats de données abandonne ce que la source savait exprimer et que la destination ne sait pas. Certaines pertes sont évidentes : les commentaires disparaissent à l'instant où du YAML devient du JSON. La plupart sont discrètes. Une date arrive sous forme de chaîne, un tableau devient cinq colonnes, un identifiant qui se terminait par 3 se termine désormais par 2. La bonne habitude consiste à savoir quel détail va partir avant qu'il ne parte.

Ce que chaque format sait transporter

CommentairesTypesImbricationDatesOrdre des clésClés en double
JSONNonchaîne, nombre, booléen, null, objet, tableauOuiAucun type natifNon ordonné selon la spécification, conservé par la plupart des analyseursIndéfini, la dernière l'emporte en général
YAMLOui, #ceux de JSON, plus les horodatages et davantage en 1.1OuiOui en 1.1, dépend des balises en 1.2Conservé dans le fichier, pas après l'analyseInterdites, mais souvent acceptées
CSVNonAucun, chaque cellule est du texteNon, uniquement des lignes platesNonFixé par la ligne d'en-têteAutorisées et ambiguës
XMLOui, <!-- -->Aucun sans schéma, typé avec XSDOuiUniquement via un schémaÉléments ordonnés, attributs nonÉléments répétés normaux, attributs en double interdits
TOMLOui, #chaîne, entier, flottant, booléen, date, heure, date-heure, tableau, tableOui, via des tablesOui, de plein droitNon significatifUne erreur, toujours

Deux colonnes causent la plupart des surprises. Les types décident si 007 survit ou arrive sous la forme du nombre 7, et l'imbrication décide si une conversion vers CSV est possible sans inventer une convention.

Le CSV est plat, et chaque cellule est du texte

Le CSV n'a qu'une seule forme : des lignes de champs. Il n'existe aucun moyen d'exprimer un objet dans une cellule, si bien qu'un convertisseur doit inventer quelque chose. En général, il aplatit avec des chemins pointés, ce qui transforme ceci :

[{ "id": 1, "name": { "first": "Ada" }, "tags": ["admin", "ops"] }]

en ceci :

id,name.first,tags.0,tags.1
1,Ada,admin,ops

Cela se déplie proprement jusqu'à ce que deux choses arrivent. Des tableaux de longueurs différentes élargissent l'en-tête jusqu'au plus long enregistrement : une ligne comportant trois étiquettes ajoute donc une colonne tags.2 et laisse une cellule vide à toutes les autres lignes. Et une clé contenant déjà un point devient ensuite indiscernable d'un chemin imbriqué, si bien que le voyage retour reconstruit la mauvaise structure. L'autre solution est une chaîne JSON dans la cellule, "[""admin"",""ops""]" avec les guillemets doublés comme l'exige le CSV, qui ne perd rien et que rien en aval ne comprend.

Les types sont l'autre moitié du problème. Une cellule contient des caractères : un lecteur doit donc deviner. Deviner donne 007 sous la forme de 7, 1.0 et 1 sous une seule valeur, un code postal sans son zéro initial, un code d'entreprise TRUE sous forme de booléen. Ne pas deviner donne chaque nombre sous forme de chaîne. L'inférence de type dans CSV vers JSON, ou dans TSV vers JSON, est une supposition assortie de valeurs par défaut sensées, pas une récupération, car les types d'origine n'ont jamais été écrits nulle part.

Une cellule vide peut signifier chaîne vide, null, zéro ou sans objet ; décidez de ce qu'elle signifie et dites-le, car le fichier en est incapable. Échanger les séparateurs, comme le fait CSV vers TSV, est ici la seule conversion sûre : il n'y a aucun type à abîmer.

YAML devine le type à votre place

YAML déduit les types d'un texte sans guillemets, ce qui le rend agréable à écrire et risqué à convertir.

country: no
version: 1.20
build: 010
start: 12:30

Sous un analyseur YAML 1.1, ce que PyYAML et bien des bibliothèques anciennes implémentent encore, cela donne :

{ "country": false, "version": 1.2, "build": 8, "start": 750 }

Les quatre ont changé de sens. no est le problème de la Norvège : YAML 1.1 traite y, yes, no, on et off comme des booléens, et le code de pays devient donc false. 1.20 est un flottant, si bien que le zéro final a disparu. 010 correspond au motif octal et devient 8. 12:30 correspond au motif sexagésimal et devient 750, un nombre de minutes.

Un analyseur au schéma de base YAML 1.2, comme le js-yaml actuel, lit le même fichier avec "no" comme chaîne, 010 comme 10 et "12:30" comme chaîne. Un seul fichier, deux analyseurs, des données différentes. Les spécifications sont réellement en désaccord ; la 1.2 a délibérément abandonné les règles sexagésimale et des mots booléens.

La défense tient en un caractère. Mettez entre guillemets tout ce qui doit être du texte : country: "no", version: "1.20". Le convertisseur YAML et JSON montre ce qu'un analyseur a fait d'un fichier, parce que le JSON n'a nulle part où cacher l'ambiguïté.

JSON n'a ni dates, ni commentaires, et plafonne à 53 bits

Les nombres JSON sont des doubles IEEE 754 : les entiers ne restent donc exacts que jusqu'à 2^53, soit 9007199254740992. Au-delà :

{ "id": 9007199254740993 }

Analysez puis resérialisez cela dans la plupart des langages et la valeur revient sous la forme 9007199254740992. Elle n'est pas représentable du tout : elle est moins arrondie qu'indisponible. C'est pourquoi Discord, Twitter et d'autres API à identifiants snowflake envoient les identifiants sous forme de chaînes, car un entier 64 bits ne tient pas dans un nombre JSON.

Il n'y a pas non plus de type date : les dates voyagent donc sous forme de chaînes, par convention en ISO 8601, et rien ne marque "2026-08-25" comme une date plutôt que comme du texte. En passant par TOML ou par un analyseur YAML 1.1, cela peut devenir une vraie date, puis revenir formaté différemment. Le JSON n'a pas non plus de commentaires ni de virgules finales, si bien que les annotations d'une source YAML ou TOML sont perdues et non déplacées. Générer des interfaces TypeScript à partir d'un échantillon hérite des deux angles morts : une chaîne n'est pas une date, et un champ absent n'est pas un champ facultatif.

XML n'entre pas dans JSON, dans aucun des deux sens

XML distingue les attributs des éléments enfants, et JSON n'a que des clés.

<user id="7" active="true">
  <name>Ada</name>
  <tag>admin</tag>
  <tag>ops</tag>
</user>

Un convertisseur choisit une convention, préfixant en général les attributs par @ et rassemblant les éléments répétés dans un tableau. C'est une perte d'un genre précis : avec un seul <tag>, il n'y a pas de tableau, si bien que la forme de la sortie dépend des données plutôt que du schéma, et un code qui attend une liste se casse sur l'enregistrement qui n'a qu'une étiquette. Un contenu mixte comme <p>Hello <b>there</b> friend</p> n'a pas d'équivalent JSON, et les espaces de noms, les sections CDATA et les commentaires disparaissent.

Le sens inverse perd d'autres choses. Un nom d'élément ne peut ni commencer par un chiffre ni contenir d'espaces : des clés JSON quelconques doivent donc être déformées. null devient un élément vide ou xsi:nil par convention. Un tableau de premier niveau exige un élément englobant qui n'existait pas dans la source. Et rien dans le JSON ne consigne si une valeur a été un attribut, si bien que JSON vers XML n'émet que des éléments. CSV vers XML fait ce choix ouvertement, en demandant des éléments ou des attributs.

Ordre des clés et clés en double

L'ordre des clés n'est garanti nulle part et préservé presque partout. La spécification JSON qualifie les objets de non ordonnés, et pourtant tous les analyseurs répandus conservent l'ordre d'insertion. Trier les clés reste plus sûr pour tout ce qui est comparé ou soumis à une somme de contrôle, car la différence entre deux documents dont les clés se sont simplement déplacées est illisible. Comparer structurellement, comme le fait l'outil de comparaison JSON, associe les valeurs par clé plutôt que par position et écarte la question.

Les clés en double sont l'arête la plus vive. Le JSON n'en dit rien : les analyseurs divergent donc et la plupart conservent silencieusement la dernière valeur. YAML les interdit et beaucoup d'analyseurs les acceptent quand même. TOML les rejette d'emblée. Le CSV autorise deux colonnes portant le même en-tête et laisse le lecteur trancher, ce qui suffit à justifier de passer un validateur CSV sur un fichier inconnu avant toute chose. XML est le seul où la répétition est porteuse de sens plutôt qu'accidentelle.

Convertir en connaissance de cause

Tout enchaînement de traitements comporte une étape avec perte. Choisissez laquelle au lieu de la découvrir plus tard.

  • Atteignez le format le plus étroit en dernier. XML vers JSON vers CSV perd les attributs à la première étape et l'imbrication à la seconde. Si le CSV est la destination, décidez quels champs comptent et aplatissez délibérément.
  • Mettez les guillemets avant la conversion, pas après. Les numéros de version, les codes de pays, les codes postaux et tout ce qui comporte un zéro initial doivent être entre guillemets dans la source. Une fois que no est devenu false, le texte d'origine est irrécupérable.
  • Vérifiez la sortie en tant que données. Formatez-la, confirmez le nombre d'enregistrements, et inspectez une ligne réputée délicate : la virgule dans un nom, le plus long tableau, le plus grand identifiant.
  • Comparez un aller-retour. Convertissez, reconvertissez, comparez. Ce que la comparaison signale est ce que la conversion ne sait pas transporter.
  • Gardez l'original. Le fichier converti est un dérivé, et quand quelqu'un demandera dans trois mois si un champ était vide ou absent, seule la source pourra répondre.