JSON vers CSV : d'où vient l'en-tête et pourquoi les valeurs changent
L’essentiel : Neuf expériences sur un vrai convertisseur : la stratégie « clés de la première ligne » perd des champs en silence, null, l'absence et la chaîne vide deviennent tous des cellules vides, un entier de 20 chiffres perd sa précision dès JSON.parse, et un point dans une clé fait entrer en collision deux chemins différents dans une même colonne. Comment l'en-tête se construit, quelles valeurs se déforment et comment choisir une stratégie selon l'usage aval.

Quand une conversion JSON vers CSV perd des données, ce n'est presque jamais pendant l'étape de « conversion ». La perte se produit dans deux questions que personne n'a posées : qui a décidé de l'en-tête, et chaque cellule reste-t-elle reconnaissable comme la valeur qu'elle était ?
Un champ disparaît en trente secondes
Nombre de convertisseurs — y compris le pipeline jq idiomatique — prennent l'en-tête du premier enregistrement uniquement. Reproduisez les conséquences en Python :
data = [{"id": 1, "name": "A"}, {"id": 2, "email": "b@x.com"}]
first_keys = list(data[0].keys()) # ['id', 'name']
rows = [[r.get(k, "") for k in first_keys] for r in data]
# L'email du deuxième enregistrement disparaît sans erreur ni avertissement
La clé email n'est pas dans l'en-tête : elle n'a même pas le droit de devenir une cellule vide — la colonne n'a jamais été créée, et la valeur est silencieusement jetée. Le correctif est l'union des clés : parcourir tous les enregistrements et créer une colonne pour chaque clé apparaissant quelque part. Les mêmes données passées par l'union donnent l'en-tête id,name,email, avec une cellule vide là où name manque, et rien n'est perdu.
Le convertisseur direct de notre page /convert/csv applique exactement cette stratégie. En entrée [{"id":1,"name":"A"},{"id":2,"email":"b@x.com"}], il produit :
id,name,email
1,A,
2,,b@x.com
Les clés sont ordonnées par première apparition et les champs absents deviennent des cellules vides. jsonoperations et jsonbeam appellent toutes deux cela « la correction du bug classique » — mais « utilise l'union » n'est que la moitié de l'histoire. L'union a son propre prix, c'est la section suivante.
Trois façons de construire un en-tête, et à qui chacune convient
| Stratégie d'en-tête | Comment elle se construit | Force | Coût |
|---|---|---|---|
| Clés de la première ligne | On ne parcourt que le premier enregistrement | Peu de colonnes, stable, une ligne de jq | Les nouveaux champs des enregistrements suivants sont perdus en silence |
| Union des clés | On parcourt tous les enregistrements | Aucun champ perdu | Colonnes éparses et beaucoup de vides quand les formes varient |
| Liste explicite de colonnes | Définie à la main ou par schéma | Totalement contrôlée, reproductible | À maintenir ; les nouveaux champs demandent une mise à jour manuelle |
Le critère de sélection n'est pas « lequel est plus sophistiqué » mais ce que l'aval fait de ce CSV :
- Un collègue qui jette un œil : les clés de la première ligne suffisent, les formes d'enregistrements sont généralement uniformes.
- Un rapport ou un pipeline de données : l'union des clés est le plancher — une cellule vide est elle-même une information.
- Un export automatique récurrent : la liste explicite de colonnes. La dérive de forme (champ ajouté en amont, clé renommée) doit échouer bruyamment, pas produire en silence un tableau que personne ne remarque faux.
La troisième option est la plus négligée. En automatisation, « la conversion a réussi » et « la conversion est correcte » sont deux affirmations différentes, et la liste explicite de colonnes est le seul mécanisme qui les sépare.
Quand une clé contient un point, les chemins d'aplatissement entrent en collision
L'aplatissement en notation pointée transforme {"a": {"b": 2}} en colonne a.b. Mais si la clé elle-même contient un point ? Testé avec pandas :
import pandas as pd
d = [{"a.b": 1, "a": {"b": 2}}]
pd.json_normalize(d, sep=".")
# Une seule colonne survit : ['a.b'] — deux clés entièrement différentes s'écrasent sur un même chemin
La clé littérale a.b et le chemin imbriqué a → b produisent le même nom de colonne, l'une écrase l'autre, et les données se corrompent en silence. C'est un exemple construit, mais cela arrive dans les données réelles : les noms de métriques de supervision (cpu.usage) et les champs de logs (error.type) adorent les points.
Deux remèdes :
- Changer le séparateur.
sep="_"ou un séparateur personnalisé, après avoir vérifié que les clés d'origine ne le contiennent jamais. - Changer la notation des chemins. JSON Pointer utilise
/comme séparateur (/a/b) ; le convertisseur Excel de toolvore emprunte cette voie. Il a la même classe d'ambiguïté — une clé contenant/doit s'écrire~1— mais les règles d'échappement sont normalisées dans la RFC 6901 plutôt qu'inventées par chaque outil.
Quel que soit votre choix, écrivez la règle dans la documentation d'export. L'en-tête CSV est la seule « schéma » que ces données portent ; le lecteur n'a nulle part ailleurs où retrouver vos règles d'aplatissement.
Les tableaux : trois stratégies, chacune perdant quelque chose de différent
Les tableaux sont le vrai obstacle du JSON vers CSV : une cellule ne peut pas contenir plusieurs valeurs. Trois stratégies, avec leurs pertes vérifiées en pratique :
| Stratégie | Forme de sortie | Ce qui se perd | Idéal pour |
|---|---|---|---|
| JSON-in-cell | Le tableau sérialisé en chaîne JSON dans la cellule | Les outils de tableaux ne peuvent ni filtrer ni agréger les éléments | Fidélité d'abord, inspection manuelle |
| Colonnes indexées | tags.0, tags.1, une colonne chacune |
Le nombre de colonnes explose avec le plus long tableau ; beaucoup de vides | Petits tableaux de longueur fixe |
| Lignes éclatées | Une ligne par élément, champs parents dupliqués | Inflation des lignes ; le comportement des tableaux vides doit être défini à part | Analyse de détail, agrégation par élément |
Nos /convert/csv et /convert/table utilisent JSON-in-cell. L'entrée [{"orderId":"A001","items":[{"sku":"X1","qty":2},{"sku":"Y9","qty":1}]}] produit :
orderId,items
A001,"[{""sku"":""X1"",""qty"":2},{""sku"":""Y9"",""qty"":1}]"
Quand vous avez besoin de lignes éclatées, pandas record_path le fait directement :
orders = [{"orderId": "A001", "total": 99,
"items": [{"sku": "X1", "qty": 2}, {"sku": "Y9", "qty": 1}]}]
items = pd.json_normalize(orders, record_path=["items"],
meta=["orderId", "total"], sep=".")
# Deux lignes de détail ; orderId et total dupliqués sur chacune
L'éclatement comporte trois pièges à trancher à l'avance : éclater plusieurs tableaux à la fois construit un produit cartésien ; les tableaux vides ne produisent aucune ligne par défaut (votre liste de clients perd des personnes) ; et les champs parents dupliqués exigent une déduplication lors de l'agrégation par orderId en aval. Le guide convertcsvonline liste « décider d'abord la granularité » comme première étape exactement pour cela — une fois la granularité fixée, les trois questions ont une seule réponse.
Testé : des valeurs qui ressortent changées de la conversion
Un test de fidélité exécuté sur notre vrai convertisseur (Node v22.13.1 + papaparse, la même implémentation que /convert/csv) :
1. null, l'absence et la chaîne vide deviennent tous des cellules vides. Entrée [{"a":null},{"b":""},{"c":1}] — les trois cellules sortent vides. Le CSV n'a pas trois états, seulement « présence de caractères » et « absence de caractères ». Après un aller-retour, les trois se relisent "" ; la sémantique d'origine est irrécupérable.
2. Les objets et tableaux imbriqués deviennent des chaînes JSON. Les guillemets sont doublés selon la RFC 4180 ({""name"":""张三""}), et coller la cellule dans n'importe quel parseur JSON restaure la valeur — c'est pourquoi JSON-in-cell préserve la fidélité.
3. Un entier de 20 chiffres perd sa précision au moment du parse, et la faute n'est pas celle du CSV. Entrée "id": 9007199254740993 ; le CSV contient déjà 9007199254740992. JSON.parse en JavaScript utilise des doubles IEEE 754 : la perte a lieu pendant le parsing, avant tout convertisseur CSV. Python json.load la préserve intégralement. Les champs de type identifiant (IDs snowflake, numéros de commande) devraient arriver en chaînes depuis le producteur. Plus sur la précision numérique dans pourquoi les grands nombres perdent leur précision.
4. Les zéros de tête des chaînes sont sûrs à la couche texte du CSV. "phone": "00123" reste 00123 dans le CSV, car c'est une chaîne du début à la fin et rien ne l'écorche. Ce qui écorche les zéros, c'est l'étape ultérieure où Excel traite la cellule comme un nombre — voir quatre façons de convertir JSON en Excel.
5. Les champs contenant virgules, guillemets ou retours à la ligne. papaparse suit la RFC 4180 : citer le champ, doubler les guillemets internes. "He said \"hi\", ok" sort en "He said ""hi"", ok" et fait l'aller-retour intact. Les sérialiseurs écrits à la main qui contournent une bibliothèque échouent presque toujours exactement là.
Aller-retour : ce que change l'interrupteur d'inférence
CSV → JSON doit deviner un type pour chaque cellule. Notre convertisseur désactive par défaut l'inférence de types et rend tout en chaînes :
[{ "id": "9007199254740992", "phone": "00123" }]
Avec typeInference activé, id devient un nombre (la précision est déjà perdue), tandis que phone reste une chaîne — les règles d'inférence excluent les formes à zéro de tête comme 010, qui ne sont pas des littéraux numériques légaux, si bien que la chaîne survit par chance.
Mémorisez cette asymétrie : l'inférence désactivée vous coûte au pire des types ; l'inférence activée peut vous coûter des données. Les colonnes qui ont besoin de nombres doivent être converties par des définitions explicites de colonnes, pas confiées à une inférence globale. L'implémentation rowsToRecords de notre article précédent JSON, CSV et données tabulaires est exactement ce principe en code.
Choisir une stratégie selon l'aval : une table de décision
| Qui reçoit le CSV | Stratégie d'en-tête | Gestion des tableaux | Types |
|---|---|---|---|
| Un collègue qui l'ouvre une fois | Union des clés | JSON-in-cell | Sans inférence |
| Excel / reporting | Union des clés | JSON-in-cell, ou colonnes indexées si longueur fixe | Colonnes identifiant forcées en texte |
| Import en base de données | Liste explicite de colonnes | Tables maître/détail séparées, ou éclatement | Définitions explicites de colonnes |
| BI / agrégation | Liste explicite de colonnes | Lignes éclatées, granularité = plus petite unité d'analyse | Définitions explicites de colonnes |
| Pipeline automatisé | Liste explicite de colonnes | JSON-in-cell, éclatement en aval | Inférence désactivée, conversion colonne par colonne |
La loi commune : plus c'est proche de l'humain, plus on peut se permettre de relâcher ; plus c'est proche de la machine, plus tout doit être explicite.
Une liste de contrôle à copier
- [ ] L'en-tête vient-il d'une union complète des clés ou d'une liste explicite ? Sinon, pouvez-vous prouver qu'aucun enregistrement n'a perdu de champ ?
- [ ] Les clés peuvent-elles contenir des points ou des barres obliques ? La collision avec le séparateur d'aplatissement a-t-elle été vérifiée ?
- [ ] La stratégie des tableaux est-elle documentée ? Le comportement des tableaux vides est-il tranché ?
- [ ] Pour
null, l'absence et la chaîne vide : que lit l'aval pour chacun ? Choisissez une règle et figez-la. - [ ] Les colonnes identifiants (IDs, numéros de commande, téléphones) voyagent-elles en chaînes de bout en bout ?
- [ ] Après éclatement, le nombre de lignes égale-t-il la somme des longueurs des tableaux ? Vérifié par échantillon ?
- [ ] Les champs d'exemple avec virgules, guillemets et retours à la ligne survivent-ils intacts à un aller-retour ?
- [ ] L'inférence globale de types est-elle désactivée à la réimportation, avec des définitions explicites pour les colonnes numériques ?
Seulement quand les huit ont une réponse nette, ce CSV est « correct », et pas simplement « converti avec succès ».
FAQ
Pourquoi le convertisseur n'aplatisse-t-il pas les objets imbriqués en colonnes par défaut ?
L'aplatissement rend le nombre de colonnes incontrôlable : l'imbrication profonde produit des noms comme user.address.geo.lat, et combinée à l'union des clés, un seul enregistrement d'une autre forme ajoute une pile de colonnes. Les chaînes JSON dans les cellules gardent la fidélité et un nombre de colonnes stable ; aplatissez explicitement quand vous en avez besoin.
Que faire si l'en-tête contient des noms de colonnes dupliqués ?
La collision de deux clés différentes après aplatissement (comme a.b contre le a→b imbriqué) doit être réglée en amont : changez de séparateur ou passez à JSON Pointer. Pour les doublons à l'intérieur d'un même objet, les convertisseurs gardent en général la dernière valeur ; les doublons d'en-tête dépendent de l'implémentation — le unparse de papaparse ne déduplique pas, et Excel fusionne ou jette silencieusement les doublons à l'ouverture. C'est pourquoi c'est dans la liste de contrôle.
Les champs parents dupliqués après éclatement sont-ils un bug ?
Non — c'est le design. Une ligne éclatée est une ligne de détail, et une ligne de détail doit pouvoir être rattachée à son enregistrement parent par clé. Agrégez en aval par la clé parente ; si la taille inquiète, exportez le fichier de détail et une table maître séparée.
Comment vérifier que ma conversion n'a perdu aucun champ ?
Faites un audit des clés : calculez l'union de toutes les clés du JSON source, puis l'ensemble des colonnes de l'en-tête CSV, et faites la différence. Une différence non vide signifie des champs perdus. Dans les pipelines par lots, encodez cela comme une assertion dans vos tests : tout changement de convertisseur qui commence à perdre des champs échouera automatiquement.