Aller au contenu
OneKitly

JSON vers CSV quand la structure est imbriquée : pourquoi il n'y a pas de bonne réponse

Publié le 17/07/2026 · 16 min de lecture · Outils pour développeurs

Daniel Okonkwo

Daniel OkonkwoDéveloppeur front-end et rédacteur Tech chez OneKitly

Performance web · Formats de fichiers

Vérifié à partir de 4 sources

Voir le profil
En bref

Aplatir du JSON imbriqué en CSV n'a pas de réponse unique, et deux convertisseurs de ce site le prouvent en divergeant sur la même entrée. Prends deux commandes, chacune avec un objet client, un tableau tags de chaînes et un tableau lines d'objets. Le convertisseur CSV / JSON / YAML produit cinq colonnes — id, customer, tags, lines, note — et réécrit chaque valeur imbriquée en texte JSON dans une seule cellule, guillemets internes doublés. Le convertisseur JSON vers CSV, réglé sur aplatir, en produit dix : id, customer.name, customer.city, tags.0, tags.1, lines.0.sku, lines.0.qty, lines.1.sku, lines.1.qty, note. Mêmes données, mêmes deux lignes, deux fois plus de colonnes, et les deux se défendent. Sérialiser préserve la forme de l'enregistrement et survit à un aller-retour machine ; aplatir rend chaque feuille triable et filtrable dans un tableur, au prix d'une disposition de colonnes dictée par le plus long tableau du fichier — une commande à trois lignes donne à toutes les commandes neuf colonnes de lignes, le plus souvent vides. Trois autres décisions n'ont pas de défaut naturel. Un tableau de scalaires devient une colonne par élément, jamais une chaîne jointe. Un tableau d'objets élargit la table ; aucun des deux convertisseurs ne l'éclate en lignes supplémentaires, ce qu'attendrait pourtant quelqu'un venu des bases de données. Et des enregistrements aux clés différentes produisent l'union des colonnes avec des cellules vides pour les trous : null, la chaîne vide et une clé absente deviennent indiscernables dès l'écriture. Un comportement est à connaître avant de faire confiance à l'un ou l'autre : le mode aplatir perd une valeur quand une clé littérale a.b rencontre un a.b imbriqué, et seule la valeur imbriquée survit.

Les deux mêmes commandes ressortent sur cinq colonnes d'un convertisseur et dix d'un autre, et aucun n'a tort. Chemins pointés, tableaux de scalaires, tableaux d'objets et enregistrements aux clés différentes : quatre décisions, prises à ta place, le plus souvent en silence.

Les deux mêmes commandes, deux fois

Voici l'entrée, et elle ne changera pas de tout l'article. Deux commandes. La première a l'id 1, un objet customer contenant name Emma et city Paris, un tableau tags de deux chaînes, et un tableau lines de deux objets, chacun avec un sku et une qty. La seconde a l'id 2, un objet customer contenant Liam et Berlin, un tableau tags d'une chaîne, un tableau lines d'un objet, et une clé supplémentaire absente du premier enregistrement : note, valant urgent. Rien d'exotique : c'est la forme de toute commande, facture ou charge utile d'événement jamais sortie d'une API.

Passe-la dans le convertisseur CSV / JSON / YAML et tu obtiens cinq colonnes : id, customer, tags, lines, note. La cellule customer de la première ligne contient les caractères {""name"":""Emma"",""city"":""Paris""} — l'objet resérialisé en JSON, puis cité comme champ CSV, ce qui double tous les guillemets qu'il contient. La cellule lines contient le tableau entier de la même façon. Ouvre cela dans un tableur et tu as deux lignes, cinq colonnes, et trois cellules que tu ne peux ni trier, ni filtrer, ni additionner.

Passe exactement le même JSON dans le convertisseur JSON vers CSV avec les valeurs imbriquées réglées sur aplatir, et tu obtiens dix colonnes : id, customer.name, customer.city, tags.0, tags.1, lines.0.sku, lines.0.qty, lines.1.sku, lines.1.qty, note. La deuxième commande n'a qu'un tag et qu'une ligne : tags.1, lines.1.sku et lines.1.qty sont vides sur cette ligne. Chaque valeur est désormais un scalaire dans sa propre colonne. Le nombre de lignes n'a pas bougé — toujours deux — et le fichier est maintenant façonné par le plus gros enregistrement plutôt que par le schéma.

Aucune des deux sorties n'est un bug. Sérialiser est le bon choix quand le CSV est un format de transport et que quelqu'un analysera ces cellules plus tard : la forme de l'enregistrement est préservée à l'identique, et un aller-retour machine restitue ce qui est entré. Aplatir est le bon choix quand un humain va ouvrir le fichier : chaque feuille est triable, filtrable, sommable. Ce qui est un bug, c'est de faire l'un ou l'autre sans savoir lequel on a fait, puis de découvrir trois semaines plus tard que l'analyste compte des lignes dans un fichier dont le nombre de lignes répond à une autre question que la sienne.

Chemins pointés, indices entre crochets, et la clé qui contient déjà un point

Une fois la décision d'aplatir prise, il faut nommer les feuilles. Deux graphies sont largement répandues. Les chemins pointés écrivent un indice de tableau comme n'importe quelle clé : tags.0, lines.1.sku. Les indices entre crochets distinguent les deux natures de pas : tags[0], lines[1].sku. L'aplatisseur JSON dédié de ce site propose les deux, plus un choix de séparateur — point, tiret bas ou barre oblique — parce qu'un tiret bas survit à un passage dans des systèmes qui traitent le point comme un opérateur de chemin, et qu'une barre oblique reprend la syntaxe de pointeur déjà connue avec JSON Pointer. Le mode aplatir du convertisseur JSON vers CSV utilise toujours le point pour les deux, la plus compacte des deux graphies et celle que les tableurs risquent le moins d'abîmer.

Une vraie défaillance se cache dans la graphie pointée, et il faut la dire clairement parce qu'elle coûte des données. Un chemin pointé est ambigu : la colonne customer.name peut désigner la clé name à l'intérieur de l'objet customer, ou une clé de premier niveau dont le nom littéral est customer.name. Le JSON autorise les deux, dans le même objet. Donne au mode aplatir un enregistrement contenant une clé littérale a.b valant 1 à côté d'un objet imbriqué a dont la clé b vaut 2, et la sortie comporte une seule colonne, a.b, contenant 2. La première valeur a disparu, sans avertissement, sans seconde colonne. C'est rare, mais ce n'est pas théorique : des clés à points apparaissent dans les champs de journaux, les noms d'événements analytiques et tout ce qui dérive d'un identifiant à espace de noms.

La parade, c'est le choix du séparateur. Aplatis avec un tiret bas ou une barre oblique au lieu du point, et la collision exige une clé qui contient littéralement ce caractère, ce qui est bien moins probable. Si tu ne peux pas choisir le séparateur — et dans le mode aplatir du convertisseur CSV tu ne le peux pas — vérifie la présence de points dans tes clés avant d'aplatir, pas après.

Les tableaux : une colonne par élément, une seule cellule, ou une ligne par élément

Un tableau de scalaires a trois réponses sensées. Le sérialiser — la cellule tags devient les caractères ["vip","eu"]. Donner une colonne à chaque élément — tags.0 et tags.1. Ou joindre les éléments avec un séparateur qui n'y figure pas, si bien que la cellule tags affiche vip|eu et qu'une formule de tableur peut la redécouper. Les convertisseurs présentés ici font les deux premières et aucun ne fait la troisième : si tu veux une chaîne jointe, il faut la produire avant la conversion. La forme jointe est la plus lisible et la seule dont le nombre de colonnes ne bouge pas quand les données changent, ce qui explique que tant d'exports l'emploient malgré son absence de définition claire.

Un tableau d'objets, c'est là que les outils et les bases de données se séparent. Avec lines à deux entrées, le mode aplatir élargit la table : lines.0.sku, lines.0.qty, lines.1.sku, lines.1.qty. Quelqu'un venu des bases attendrait l'inverse — une ligne de sortie par article, avec les champs de la commande répétés sur le bloc, ce que produit une jointure et ce qu'attend un tableau croisé. Aucun des deux convertisseurs ne le fait, et la différence n'est pas cosmétique. Élargir conserve une ligne par commande : un décompte de lignes est un décompte de commandes. Éclater donne une ligne par article : un décompte de lignes est un décompte d'articles et les champs de commande sont dupliqués. Les deux existent ; une seule répond à la question combien de commandes avons-nous expédiées.

Une conséquence de l'élargissement mérite d'être anticipée : la disposition des colonnes est fixée par le plus grand tableau du fichier, et elle change quand les données changent. Deux enregistrements dont les tableaux de tags comptent un et trois éléments produisent les colonnes t.0, t.1 et t.2, avec deux cellules vides sur l'enregistrement court. Exporte la même requête demain avec un enregistrement à quatre tags et le fichier gagne une colonne, en silence. Tout ce qui, en aval, lit les colonnes par position plutôt que par nom casse ce jour-là, et l'export qui l'a cassé ressemble trait pour trait au précédent.

Des enregistrements qui ne s'accordent pas sur leurs clés

Le JSON n'a pas de schéma : un tableau d'objets n'est pas une table tant qu'on n'en fait pas une. Les deux convertisseurs prennent l'union de toutes les clés rencontrées et laissent un trou là où un enregistrement n'en possède pas. Trois enregistrements portant {id, a}, {id, b} et {id, a, c} donnent quatre colonnes — id, a, b, c — avec des cellules vides là où chacun est muet. C'est la seule réponse qui ne perd rien, et c'est pourquoi un CSV exporté d'une base documentaire est souvent bien plus large que n'importe lequel de ses documents.

L'ordre des colonnes n'est ni trié ni stable d'un export à l'autre. Les deux convertisseurs utilisent l'ordre de première apparition : les clés se présentent dans l'ordre où le premier enregistrement qui les contient les expose. Deux enregistrements {b, a} et {a, b} produisent les colonnes b puis a, parce que le premier a été lu en premier. Change le tri de ta requête et l'ordre des colonnes change avec, alors que les données sont identiques. Si quelque chose en aval dépend de l'ordre des colonnes, trie les clés toi-même avant l'export.

Une chose est perdue quelle que soit la stratégie retenue, et le coupable est le CSV, pas le convertisseur. Une clé valant null, une clé valant la chaîne vide et une clé simplement absente deviennent toutes la même cellule vide. Relis ce CSV et chacune revient sous forme de chaîne vide. Si la distinction compte — et dans une mise à jour partielle ou une colonne annulable elle compte toujours — le CSV n'est pas le bon format pour ce champ, et aucune option d'aplatissement ne le sauvera.

Ce que le côté CSV détecte, et ce qu'il ne détecte pas

Commence par le sens qui faisait honte à ce convertisseur. Donne au convertisseur CSV / JSON / YAML le JSON [1,2,3] et il renvoie value / 1 / 2 / 3 : une seule colonne synthétisée, parce qu'un nombre n'a pas de clé propre pour devenir un nom de colonne et que value est le seul nom honnête. Un scalaire de premier niveau, 42, donne value / 42. Un tableau mixte, [1,{"a":2}], donne l'en-tête value,a et deux lignes — 1 suivi d'une cellule vide, puis une cellule vide suivie de 2 — le scalaire dans la colonne inventée, l'objet dans la sienne. C'est exactement la réponse du convertisseur frère JSON vers CSV : les deux outils du site s'accordent désormais sur le cas qui arrive le plus souvent d'une API renvoyant une simple liste d'identifiants.

En lecture, le convertisseur renifle désormais son délimiteur au lieu de supposer la virgule : il compte les virgules contre les points-virgules sur le premier enregistrement, en ignorant ce qui est entre guillemets, si bien que name;city sur Emma;Paris arrive en {"name":"Emma","city":"Paris"} — ce qui compte, car Excel écrit du CSV à points-virgules par défaut dans cinq des six marchés de ce site. Les noms de colonne répétés sont renommés plutôt que supprimés : name,name,name sur a,b,c renvoie name, name_2 et name_3. Une ligne qui dépasse l'en-tête garde la cellule en trop sous un nom inventé : a,b sur 1,2,3 renvoie a, b et column3. Ce qu'il ne fait toujours pas, c'est évaluer une tabulation ou une barre verticale. L'outil dédié CSV vers JSON pèse quatre candidats et te laisse en imposer un ; ce convertisseur en pèse deux et n'offre aucun réglage de délimiteur dans son interface, si bien qu'un fichier séparé par des tabulations arrive encore en une seule colonne dont la clé est toute la ligne d'en-tête. Et quoi qu'il ait reçu, le CSV qu'il réécrit est séparé par des virgules.

Un enregistrement imbriqué, passé dans les deux convertisseurs — les sorties sont celles réellement produites
Valeur imbriquéeSérialisé (convertisseur CSV / JSON / YAML)Aplati (JSON vers CSV, mode aplatir)La décision prise à ta place
Un objet : customer = {name, city}Une colonne, customer, contenant le texte JSONDeux colonnes, customer.name et customer.cityAller-retour machine, ou tri humain — pas les deux
Un tableau de chaînes : tags = [vip, eu]Une colonne contenant ["vip","eu"]Deux colonnes, tags.0 et tags.1Aucun ne les joint en vip|eu ; si tu le veux, construis-le avant de convertir
Un tableau d'objets : lines = deux articlesUne colonne contenant tout le tableau en texte JSONQuatre colonnes : lines.0.sku, lines.0.qty, lines.1.sku, lines.1.qtyAucun ne l'éclate en une ligne par article : le nombre de lignes reste un nombre de commandes
Une clé que seul le second enregistrement possède : noteUne colonne note, vide sur la première ligneIdem : une colonne note, vide sur la première ligneUnion des colonnes avec des trous — après quoi null, vide et absent sont la même cellule
Une clé littérale a.b à côté d'un a imbriqué avec une clé bDeux colonnes, a.b et a — les deux valeurs surviventUne colonne, a.b, portant la valeur imbriquée ; la valeur de la clé littérale est perdueUn chemin pointé ne distingue pas un pas d'un nom qui contient un point
Un tableau de scalaires de premier niveau : [1,2,3]Une colonne unique nommée value, portant 1, 2 et 3Idem : une colonne unique nommée valueUn scalaire n'a pas de clé pour devenir un nom de colonne ; les deux outils en inventent désormais le même
Convertisseur CSV / JSON / YAMLConvertis entre CSV, TSV, JSON et YAML, et vois les lignes en tableau.Essayer l'outil

Questions fréquentes

Faut-il sérialiser les valeurs imbriquées ou les aplatir ?
Demande qui ouvre le fichier. Si c'est un programme qui va le réanalyser, sérialise : la forme de l'enregistrement est préservée à l'identique et l'aller-retour restitue ce qui est entré. Si c'est une personne dans un tableur, aplatis : elle doit trier par customer.city et sommer lines.0.qty, et elle ne peut faire ni l'un ni l'autre avec un bloc JSON dans une cellule. Si c'est les deux, produis deux fichiers plutôt qu'un compromis, car le compromis — aplatir un niveau et sérialiser le reste — est la version que personne ne saura interpréter six mois plus tard. Et si le fichier est une archive plutôt qu'un rapport, sérialise : aplatir fige les longueurs de tableaux d'aujourd'hui dans la disposition des colonnes, et l'export du mois prochain n'y correspondra pas.
Puis-je obtenir une ligne par article plutôt que des colonnes supplémentaires ?
Aucun des deux convertisseurs ne le fait : tous deux élargissent la table et aucun n'éclate un tableau en lignes. La raison est qu'éclater n'est pas un choix de mise en forme mais un changement de grain : le fichier obtenu répond à une autre question, et le convertisseur devrait décider quel tableau éclater quand un enregistrement en contient deux. Fais-le avant la conversion, dans ce qui produit le JSON : émets un objet par article, portant chacun les champs de commande dont il a besoin. La question de l'aplatissement disparaît alors, puisque le tableau n'existe plus. Si tu n'as que le JSON, un court script qui associe chaque commande à ses articles et concatène les résultats tient en cinq lignes et laisse la décision visible dans ton code plutôt qu'enfouie dans les réglages d'un outil.
Pourquoi mon fichier a-t-il gagné une colonne entre deux exports de la même requête ?
Parce que la disposition des colonnes d'un export aplati est une propriété des données, pas de la requête. Les colonnes sont l'union de tous les chemins présents, et les chemins de tableaux sont numérotés jusqu'à la longueur du plus long tableau du résultat. Un enregistrement à quatre tags, là où la fois précédente n'en comptait que trois au plus, ajoute tags.3 à toutes les lignes. Le même mécanisme ajoute une colonne quand un seul enregistrement contient une clé facultative que personne n'avait encore utilisée. Deux parades : lire les colonnes par nom et non par position partout en aval, et, si une disposition stable compte vraiment, définir explicitement la liste des colonnes et projeter dessus, plutôt que de laisser l'exporteur la déduire de ce qui se trouvait dans le jeu de résultats.
Puis-je retransformer le CSV aplati en JSON d'origine ?
En partie, et les manques sont prévisibles. L'aplatisseur JSON dispose d'un mode reconstruire qui rebâtit l'imbrication à partir des clés-chemins : un segment qui n'est qu'un nombre construit un tableau, tout le reste construit un objet, si bien que customer.name et tags.0 reviennent en objet et en tableau. Trois choses ne reviennent pas. Les types ont disparu, puisque chaque cellule d'un CSV est du texte — un nombre écrit 1 revient en chaîne "1" tant que tu ne le convertis pas. La distinction entre null, chaîne vide et clé absente a disparu, comme vu plus haut. Et un tableau vide ou un objet vide ne laisse aucun chemin dans un aplatissement pointé : il est donc irrécupérable. L'aplatisseur écrit un [] ou un {} visible précisément pour cela, mais seulement si tu as aplati avec cet outil. Faire l'aller-retour par la stratégie de sérialisation ne perd rien de tout cela, et c'est tout son argument.
Mon CSV utilise des points-virgules. Dois-je le convertir d'abord ?
Non. Le convertisseur CSV / JSON / YAML compte les virgules contre les points-virgules sur le premier enregistrement, hors des champs entre guillemets, et retient le gagnant : un export Excel français, allemand, espagnol, italien ou portugais se lit correctement sans rien régler. Le cas qui échoue encore, c'est la tabulation ou la barre verticale : aucune des deux ne figure parmi les candidats évalués, donc un fichier séparé par des tabulations donne un seul champ par ligne, dont la clé est toute ta ligne d'en-tête. Le symptôme est reconnaissable une fois qu'on le connaît : une clé unique avec des tabulations dans son nom. Deux issues. Convertis d'abord le délimiteur avec le convertisseur de délimiteurs, qui analyse proprement et remet les guillemets là où il faut. Ou utilise l'outil dédié CSV vers JSON, qui pèse quatre candidats et te laisse en imposer un. À noter aussi : quoi qu'il reçoive, le CSV que ce convertisseur réécrit est séparé par des virgules.

Articles qui pourraient t'intéresser

Tous les guides
ExplicationCSV vers JSON : les cinq cas qui cassent tous les convertisseursDélimiteurs entre guillemets, sauts de ligne intégrés, types ambigus, en-têtes en double et encodage. Chaque cas a été passé dans le convertisseur et la sortie exacte est reproduite ici — y compris les deux qu'il ne rattrape pas.TutorielCombien de temps pour télécharger un fichier ? Durée, bits et octets, surcoûtEstime le temps de téléchargement à partir de la taille du fichier et du débit. Découvre la formule taille ÷ débit, la conversion cruciale bits/octets (diviser par 8) et pourquoi les téléchargements réels sont plus lents que prévu.ExplicationJSON est plus simple que tu ne le crois, et c'est le problèmeJSON n'a ni type entier, ni type date, ni commentaires, ni schéma. Chacune de ces absences produit un bogue précis : un identifiant à 19 chiffres revient faussé de 21, un horodatage devient une chaîne sur laquelle personne ne s'est accordé, NaN ne s'écrit pas, et les clés dupliquées sont légales. Le tout exécuté, dans deux langages.TutorielComment convertir du JSON en CSV : aplatir des tableaux d'objets en lignes et colonnesUn guide pratique pour transformer un tableau JSON d'objets en fichier CSV propre, avec l'aplatissement des champs imbriqués et la gestion des cas limites.ExplicationPoint-virgule, tabulation, barre verticale : choisir un délimiteur qui survit au trajetPourquoi la langue du lecteur décide du délimiteur, ce que le convertisseur fait aux guillemets quand tu changes, ce qu'est réellement la première ligne sep=, et le nombre de cellules citées sur le même export écrit de cinq façons.GuideTransposer un tableau dont les lignes auraient dû être des colonnesCe qu'il advient de la ligne d'en-tête, des lignes de longueurs inégales, des types — et la seule chose pour laquelle on confond régulièrement la transposition et qu'elle ne sait pas faire.

Outils similaires

Ceci décrit ce que font ces convertisseurs aujourd'hui, vérifié en les exécutant, et non ce qu'une norme imposerait à un convertisseur. Le CSV n'a pas de norme prescriptive : la RFC 4180 est informative et décrit un usage courant, si bien que deux outils apparemment corrects peuvent diverger sur le même fichier sans qu'aucun ait tort. L'aplatissement, la détection de types et celle des tableaux sont des conventions, pas des règles. Avant de convertir des données que tu ne pourras pas réexporter, passe d'abord sur une copie et compare le nombre de lignes et de colonnes aux deux bouts.

Sources

Tu as repéré une erreur dans cet article ?