Aller au contenu
jsonbeautifiers
Français

Aplatir du JSON imbriqué en CSV sans perdre de données

Chaque convertisseur JSON vers CSV prend pour vous une douzaine de décisions non documentées. Voici ces décisions.

Chaque affirmation de cette page est soit mesurée, soit sourcée. Quand elle n’est ni l’une ni l’autre, la page le dit.

Passez ceci dans à peu près n’importe quel convertisseur :

[
  { "id": 1, "name": "Ada", "tags": ["admin"] },
  { "id": 2, "name": "Grace", "tags": ["admin", "ops"], "team": { "name": "core" } }
]

Beaucoup vous rendent trois colonnes : id, name, tags. L’objet team a disparu. Ni tronqué, ni signalé, simplement absent, parce que le convertisseur a lu les clés du premier objet et les a prises pour le schéma.

Le CSV est un rectangle : un jeu de colonnes fixe, un scalaire par cellule. Le JSON est un arbre avec des clés facultatives, une profondeur quelconque et des tableaux partout. Il n’existe pas de correspondance correcte entre les deux, seulement un ensemble de politiques, et les convertisseurs qui paraissent simples sont ceux qui ont choisi les politiques à votre place sans le dire.

Découverte des colonnes : l’union, pas la première ligne

Deux façons de décider quelles sont les colonnes. Parcourir toutes les lignes et rassembler l’union des chemins feuilles, ou lire un objet et prendre ses clés.

La seconde n’est pas une optimisation de performance, c’est une perte de données avec une excuse plausible. Papa Parse tire ses champs des clés du premier objet, sauf si vous passez une option columns explicite :

Papa.unparse([{ a: 1 }, { a: 2, b: 3 }]);
// "a\r\n1\r\n2"   la colonne b n’a jamais existé

Papa.unparse(rows, { columns: ['a', 'b'] });
// c’est vous qui fournissez l’union

json_normalize de pandas prend l’union, ce qui explique en partie pourquoi les gens s’en servent. Le coût, c’est qu’un document creux produit une table large et presque vide, ce qui est la représentation honnête d’un document creux. Si vous voulez moins de colonnes, supprimez-les délibérément.

Le streaming rend la chose réellement difficile : avec NDJSON, vous ne connaissez le jeu de colonnes qu’à la dernière ligne, donc soit vous mettez le fichier en mémoire, soit vous faites deux passes.

Objets imbriqués et le séparateur qu’il faut exposer

Les objets imbriqués s’aplatissent en chemins pointés, donc {"team": {"name": "core"}} devient team.name. pandas utilise . par défaut et vous laisse le changer :

pd.json_normalize({"user": {"name": {"first": "Ada"}}})
# colonne : user.name.first

pd.json_normalize(data, sep="__")
# colonne : user__name__first

Le séparateur doit être configurable, parce que le point est un caractère légal dans une clé JSON. Ces deux documents s’aplatissent vers la même colonne :

{ "a": { "b": 1 } }
{ "a.b": 1 }

Une fois qu’ils entrent en collision, le trajet retour relève de la devinette, et un convertisseur qui laisse l’un écraser l’autre en silence a produit un fichier qui se reconstruit dans la mauvaise forme. Choisissez un séparateur absent de vos clés, ou échappez-le là où il apparaît dans l’une d’elles. Ne supposez pas que les points n’apparaissent jamais dans les clés : dans les payloads d’événements et d’analytique, ils apparaissent en permanence.

Tableaux : quatre politiques, une valeur par défaut raisonnable

C’est là que les convertisseurs divergent le plus.

Politique Sortie pour tags: ["admin","ops"] Ce que ça coûte
Colonnes indexées tags.0 = admin, tags.1 = ops Le nombre de colonnes est fixé par le plus long tableau du fichier. Une ligne à 400 tags donne 400 colonnes à toutes les lignes
Jointure dans une cellule tags = admin,ops Casse dès qu’une valeur contient le caractère de jointure, et [] et [""] s’affichent pareil
JSON dans une cellule tags = ["admin","ops"] Laid, exige un guillemetage correct, survit exactement à l’aller-retour
Éclatement en lignes Deux lignes, les autres champs répétés Le nombre de lignes ne correspond plus au nombre d’enregistrements, donc les agrégats sur les autres colonnes comptent double

Les colonnes indexées conviennent à une arité petite et fixe : un couple latitude/longitude, un triplet RVB. Pour tout ce qui n’est pas borné, le nombre de colonnes est décidé par votre pire ligne plutôt que par la ligne typique.

La jointure est la valeur par défaut la plus répandue et la pire, avec pertes dans trois directions à la fois : le délimiteur peut apparaître dans les données, le tableau vide et le tableau contenant une chaîne vide se confondent, et les objets imbriqués finissent transformés en texte de toute façon.

JSON dans une cellule est la bonne valeur par défaut pour les tableaux que vous n’éclatez pas, parce que c’est la seule politique exactement réversible. La cellule est guillemetée selon la RFC 4180 avec les guillemets internes doublés, et tout lecteur qui sait que la colonne contient du JSON le réanalyse directement. C’est plus vilain dans Excel et c’est correct.

L’éclatement convient quand le tableau est le sujet : les lignes d’une commande, les événements d’une session. C’est ce que fait record_path :

import pandas as pd

data = [
    {"id": 1, "name": "Ada",   "orders": [{"sku": "A1", "qty": 2}]},
    {"id": 2, "name": "Grace", "orders": [{"sku": "B7", "qty": 1},
                                          {"sku": "C3", "qty": 5}]},
]

pd.json_normalize(data, record_path="orders", meta=["id", "name"])
#   sku  qty  id   name
# 0  A1    2   1    Ada
# 1  B7    1   2  Grace
# 2  C3    5   2  Grace

record_path nomme le tableau à transformer en lignes et meta nomme les champs du parent recopiés sur chacune. Regardez ce qui arrive à un enregistrement dont le tableau orders est vide : il ne produit aucune ligne et disparaît entièrement. Vous obtenez aussi un tableau par passe, puisque deux tableaux frères exigeraient un produit cartésien : faites donc une passe par tableau et joignez sur l’identifiant.

Tableaux hétérogènes

Un tableau dont les objets ont des clés différentes, c’est le même problème d’union un niveau plus bas. Avec les colonnes indexées, [{"a":1},{"b":2}] donne items.0.a et items.1.b, deux colonnes jamais remplies ensemble, le jeu de colonnes dépendant désormais de la position de l’élément. Avec l’éclatement, on obtient deux lignes avec les colonnes a et b, ce qui vaut mieux parce que la position cesse de faire partie de l’identité. Les tableaux qui mêlent scalaires et objets n’ont aucune forme rectangulaire ; sérialisez-les en JSON dans une cellule.

Valeurs sans équivalent CSV

Le CSV a un seul type : le texte. Tout le reste est convention.

null contre chaîne vide. JSON les distingue, le CSV non : ,, et ,"", sont la même valeur pour la plupart des lecteurs, donc l’aller-retour fond l’un dans l’autre. Si cela compte, écrivez une sentinelle comme \N (la convention COPY de Postgres), ou acceptez que les nuls reviennent en chaînes vides et dites-le.

Booléens. true et false en minuscules, c’est l’orthographe JSON et elle survit. Excel affiche TRUE/FALSE et certains outils émettent 1/0, ce qui exige dans les deux cas un mappage explicite au retour.

Nombres. Une chaîne JSON contenant 007 est lue par Excel comme 7, et 1E5 devient 100000. Le guillemetage CSV n’empêche rien, puisque Excel devine le type après avoir retiré les guillemets. Les grands entiers heurtent la limite de précision si quoi que ce soit dans la chaîne les fait transiter par un flottant : émettez donc le texte source du nombre tel quel.

Dates. JSON n’a pas de type date ; les chaînes ISO 8601 ou RFC 3339 sont la convention. Excel convertit une chaîne qui ressemble à une date comme 2026-03-04 en valeur de date et la réaffiche au format local de la machine, et des formats ambigus comme 03/04/2026 peuvent revenir comme un tout autre jour : ne laissez jamais un tableur servir d’étape intermédiaire.

La mécanique CSV qui mord

La RFC 4180 est courte et mérite d’être suivie. Les champs contenant une virgule, un guillemet double ou un saut de ligne doivent être guillemetés ; un guillemet double littéral à l’intérieur d’un champ guillemeté s’écrit deux fois ; les fins de ligne sont des CRLF. Les sauts de ligne intégrés dans un champ guillemeté sont légaux, et bon nombre de lecteurs CSV s’y trompent encore : si vos chaînes contiennent des sauts de ligne, testez d’abord le consommateur.

Le délimiteur n’est pas toujours une virgule. Excel, dans une locale où le séparateur décimal est la virgule, attend des fichiers séparés par des points-virgules, et c’est pourquoi un CSV valide s’ouvre sur une seule colonne chez un collègue. Proposez un réglage de délimiteur, ou livrez la première ligne sep=; qu’Excel comprend.

Ensuite, la marque d’ordre des octets : Excel ne lit un CSV en UTF-8 comme de l’UTF-8 que si le fichier commence par une BOM, et sans elle les caractères accentués sont décodés avec la page de codes du système et massacrés. Ces trois octets sont du bruit pour tous les autres outils : faites de la BOM une bascule et activez-la pour le chemin Excel.

Injection CSV

Si le premier caractère d’une cellule est =, +, - ou @, Excel, Google Sheets et LibreOffice traitent la cellule comme une formule et l’évaluent à l’ouverture. L’OWASP appelle cela l’injection CSV. Certaines recommandations ajoutent la tabulation et le retour chariot à la liste des déclencheurs.

Vous ne pouvez pas vous en sortir avec des guillemets : le lecteur retire les guillemets RFC 4180 avant d’évaluer la formule. Donc si une chaîne de votre JSON vient d’un utilisateur et atterrit dans un CSV que quelqu’un ouvre, vous avez remis à un attaquant une formule qui s’exécute dans un contexte de confiance. Les formules peuvent aller chercher des URL distantes, ce qui veut dire que les cellules voisines peuvent quitter l’immeuble.

L’atténuation consiste à neutraliser le caractère de tête au moment d’écrire la cellule :

const RISKY = /^[=+\-@\t\r]/;

function safeCell(value) {
  const s = String(value);
  return RISKY.test(s) ? "'" + s : s;
}

L’apostrophe force Excel à traiter le contenu comme du texte. Ce n’est pas gratuit : pour un lecteur qui n’est pas un tableur, elle fait désormais partie des données, donc l’aller-retour est cassé pour ces valeurs. Le préfixage convient aux fichiers qu’un humain ouvre dans un tableur et ne convient pas aux fichiers qu’une machine relit, ce qui en fait une bascule par export plutôt qu’un défaut caché.

Le trajet dans l’autre sens

CSV vers JSON comporte un gros piège : l’inférence de type. Toute valeur du fichier est du texte, donc le convertisseur devine lesquelles sont des nombres et se trompe de façon prévisible. 007 devient 7, 1E5 devient 100000, 1.0 devient 1. Codes postaux, références de pièces, numéros de téléphone et chaînes de version meurent tous de la même règle. La valeur par défaut sûre est d’émettre chaque valeur en chaîne et de laisser l’appelant convertir ce qu’il connaît, avec une inférence activable par colonne plutôt qu’une heuristique appliquée à tout le fichier. L’outil CSV vers JSON rend cette bascule explicite exactement pour cette raison.

Une politique par défaut qui mérite d’être énoncée

Décision Par défaut Pourquoi
Colonnes Union de tous les chemins feuilles, triée L’échantillonnage sur le premier objet supprime des champs sans prévenir
Objets imbriqués Chemin pointé, séparateur configurable Les clés peuvent légalement contenir le séparateur
Tableaux JSON dans une cellule La seule politique réversible. Éclatez quand le tableau est l’enregistrement
Conteneurs vides [] et {} littéralement Distinguables de null et de la chaîne vide
Nuls Cellule vide, documenté Ou une sentinelle là où la distinction porte du sens
Nombres Texte source tel quel Ne les faites jamais transiter par un flottant à la sortie
Fins de ligne CRLF RFC 4180, et LF casse plus de lecteurs que CRLF
BOM Désactivée, avec une bascule Excel Bon pour les pipelines, mauvais pour Excel : laissez l’utilisateur trancher
Caractères de formule Préfixés uniquement sur le chemin Excel Le préfixage modifie les données, il ne doit donc pas se produire en silence

Ce ne sont pas les seules réponses défendables. Le fait est que tout convertisseur prend les neuf décisions, qu’il vous le dise ou non, et seuls ceux qui vous le disent méritent qu’on leur confie un payload trop gros pour être relu à l’œil. L’aplatisseur montre le jeu de chemins avant que vous ne vous engagiez, et la vue tableau montre le rectangle que vous êtes sur le point d’obtenir.