Vai al contenuto
jsonbeautifiers
Italiano

Appiattire JSON annidato in CSV senza perdere dati

Ogni convertitore da JSON a CSV prende per te una dozzina di decisioni non documentate. Queste sono le decisioni.

Ogni affermazione di questa pagina è misurata oppure ha una fonte. Quando non è né l’una né l’altra, la pagina lo dice.

Passa questo in quasi qualsiasi convertitore:

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

Molti ti restituiscono tre colonne: id, name, tags. L’oggetto team è sparito. Non troncato, non segnalato, semplicemente assente, perché il convertitore ha letto le chiavi del primo oggetto e le ha prese per lo schema.

Il CSV è un rettangolo: un insieme fisso di colonne, uno scalare per cella. Il JSON è un albero con chiavi opzionali, profondità arbitraria e array ovunque. Non esiste una corrispondenza corretta tra i due, solo un insieme di criteri, e i convertitori che sembrano semplici sono quelli che hanno scelto i criteri al posto tuo senza dirlo.

Scoperta delle colonne: l’unione, non la prima riga

Ci sono due modi di decidere quali siano le colonne. Percorrere tutte le righe e raccogliere l’unione dei percorsi foglia, oppure leggere un oggetto e prenderne le chiavi.

Il secondo non è un’ottimizzazione di prestazioni, è perdita di dati con una scusa plausibile. Papa Parse ricava i campi dalle chiavi del primo oggetto, a meno che tu non passi un’opzione columns esplicita:

Papa.unparse([{ a: 1 }, { a: 2, b: 3 }]);
// "a\r\n1\r\n2"   la colonna b non è mai esistita

Papa.unparse(rows, { columns: ['a', 'b'] });
// l’unione la fornisci tu

json_normalize di pandas prende l’unione, ed è uno dei motivi per cui la gente ci ricorre. Il costo è che un documento sparso produce una tabella larga e quasi vuota, che è la rappresentazione onesta di un documento sparso. Se vuoi meno colonne, eliminale apposta.

Lo streaming rende la cosa davvero difficile: con NDJSON non conosci l’insieme delle colonne fino all’ultima riga, quindi o metti il file in memoria o fai due passate.

Oggetti annidati e il separatore che devi esporre

Gli oggetti annidati si appiattiscono in percorsi puntati, quindi {"team": {"name": "core"}} diventa team.name. pandas usa . come impostazione predefinita e ti lascia cambiarlo:

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

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

Il separatore deve essere configurabile, perché il punto è un carattere lecito in una chiave JSON. Questi due documenti si appiattiscono nella stessa colonna:

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

Una volta che collidono, il viaggio di ritorno è tirare a indovinare, e un convertitore che lascia che l’uno sovrascriva l’altro in silenzio ha prodotto un file che si ricostruisce nella forma sbagliata. O scegli un separatore assente dalle tue chiavi, oppure fanne l’escape dove compare dentro una di esse. Non dare per scontato che i punti non compaiano mai nelle chiavi: nei payload di eventi e analytics compaiono di continuo.

Array: quattro criteri, una sola impostazione sensata

È qui che i convertitori divergono di più.

Criterio Output per tags: ["admin","ops"] Quanto costa
Colonne indicizzate tags.0 = admin, tags.1 = ops Il numero di colonne lo fissa l’array più lungo del file. Una riga con 400 tag dà 400 colonne a tutte le righe
Unione in una cella tags = admin,ops Si rompe appena un valore contiene il carattere di unione, e [] e [""] appaiono identici
JSON in una cella tags = ["admin","ops"] Brutto, richiede virgolettatura corretta, sopravvive all’andata e ritorno in modo esatto
Esplosione in righe Due righe, gli altri campi ripetuti Il numero di righe non corrisponde più a quello dei record, quindi le aggregazioni sulle altre colonne contano doppio

Le colonne indicizzate vanno bene con arietà piccola e fissa: una coppia lat/long, una terna RGB. Per qualsiasi cosa illimitata il numero di colonne lo decide la tua riga peggiore anziché quella tipica.

Unire è l’impostazione predefinita più diffusa e la peggiore, con perdita in tre direzioni insieme: il delimitatore può comparire nei dati, array vuoto e array con stringa vuota collassano, e gli oggetti annidati finiscono comunque trasformati in testo.

JSON in una cella è l’impostazione giusta per gli array che non esplodi, perché è l’unico criterio esattamente reversibile. La cella viene virgolettata secondo la RFC 4180 con le virgolette interne raddoppiate, e qualsiasi lettore che sappia che la colonna contiene JSON lo rianalizza direttamente. In Excel si vede peggio, ed è corretto.

Esplodere è giusto quando l’array è il punto: le righe di un ordine, gli eventi di una sessione. È quello che fa 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 nomina l’array da trasformare in righe e meta nomina i campi del genitore ricopiati su ciascuna. Guarda cosa succede a un record il cui array orders è vuoto: non produce righe e sparisce del tutto. Inoltre ottieni un array per passata, dato che due array fratelli richiederebbero un prodotto cartesiano: fai quindi una passata per array e uniscile sull’id.

Array eterogenei

Un array i cui oggetti hanno chiavi diverse è lo stesso problema dell’unione un livello più in basso. Con le colonne indicizzate, [{"a":1},{"b":2}]items.0.a e items.1.b, due colonne mai riempite entrambe, con l’insieme delle colonne che ora dipende dalla posizione dell’elemento. Con l’esplosione dà due righe con colonne a e b, il che è meglio perché la posizione smette di far parte dell’identità. Gli array che mescolano scalari e oggetti non hanno alcuna forma rettangolare; serializzali come JSON in una cella.

Valori senza equivalente in CSV

Il CSV ha un solo tipo: testo. Tutto il resto è convenzione.

null contro stringa vuota. Il JSON li distingue, il CSV no: ,, e ,"", sono lo stesso valore per la maggior parte dei lettori, quindi l’andata e ritorno fa collassare l’uno nell’altro. Se la cosa conta, scrivi una sentinella come \N (la convenzione COPY di Postgres), oppure accetta che i null tornino come stringhe vuote e dichiaralo.

Booleani. true e false minuscoli è la grafia JSON e sopravvive. Excel mostra TRUE/FALSE e alcuni strumenti emettono 1/0, e in entrambi i casi serve una mappatura esplicita al ritorno.

Numeri. Una stringa JSON che contiene 007 viene letta da Excel come 7, e 1E5 diventa 100000. La virgolettatura CSV non lo impedisce, dato che Excel indovina il tipo dopo aver tolto le virgolette. Gli interi grandi urtano il confine di precisione se qualcosa nella catena li instrada attraverso un float: emetti quindi il testo sorgente del numero alla lettera.

Date. Il JSON non ha un tipo data; le stringhe ISO 8601 o RFC 3339 sono la convenzione. Excel converte una stringa dall’aria di data come 2026-03-04 in un valore di data e la rimostra nel formato locale della macchina, e formati ambigui come 03/04/2026 possono tornare come un giorno del tutto diverso: non lasciare mai che un foglio di calcolo faccia da tappa intermedia.

Meccaniche del CSV che mordono

La RFC 4180 è breve e vale la pena seguirla. I campi che contengono una virgola, una virgoletta doppia o un a capo devono essere virgolettati; una virgoletta doppia letterale dentro un campo virgolettato si scrive due volte; i fine riga sono CRLF. Gli a capo incorporati in un campo virgolettato sono leciti e parecchi lettori CSV continuano a sbagliarli: se le tue stringhe contengono a capo, prova prima il consumatore.

Il delimitatore non è sempre una virgola. Excel, in una localizzazione in cui il separatore decimale è la virgola, si aspetta file separati da punto e virgola, ed è per questo che un CSV valido si apre su una sola colonna sulla macchina di un collega. Offri un’impostazione per il delimitatore, oppure spedisci la prima riga sep=; che Excel capisce.

Poi il byte order mark: Excel legge un CSV in UTF-8 come UTF-8 solo se il file inizia con un BOM, e senza di quello i caratteri accentati vengono decodificati con il codepage di sistema e sfigurati. Quei tre byte sono rumore per ogni altro strumento: rendi il BOM un interruttore e accendilo per il percorso Excel.

CSV injection

Se il primo carattere di una cella è =, +, - o @, Excel, Google Sheets e LibreOffice trattano la cella come una formula e la valutano all’apertura. OWASP la chiama CSV injection. Alcune linee guida aggiungono tabulazione e ritorno a capo all’elenco degli inneschi.

Non te la cavi con le virgolette: il lettore toglie le virgolette RFC 4180 prima che la formula venga valutata. Quindi se una qualsiasi stringa del tuo JSON viene da un utente e finisce in un CSV che qualcuno apre, hai consegnato a un attaccante una formula che gira in un contesto fidato. Le formule possono richiedere URL remoti, il che significa che le celle vicine possono uscire dall’edificio.

La mitigazione è neutralizzare il carattere iniziale mentre scrivi la cella:

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

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

L’apostrofo costringe Excel a trattare il contenuto come testo. Non è gratis: per un lettore che non è un foglio di calcolo ora fa parte del dato, quindi per quei valori l’andata e ritorno si rompe. Il prefisso è giusto per i file che una persona apre in un foglio di calcolo e sbagliato per i file che una macchina rilegge, il che ne fa un interruttore per esportazione anziché un valore predefinito nascosto.

Il percorso inverso

Da CSV a JSON c’è una grossa trappola: l’inferenza dei tipi. Ogni valore del file è testo, quindi il convertitore indovina quali siano numeri e sbaglia in modo prevedibile. 007 diventa 7, 1E5 diventa 100000, 1.0 diventa 1. CAP, codici articolo, numeri di telefono e stringhe di versione muoiono tutti per la stessa regola. L’impostazione sicura è emettere ogni valore come stringa e lasciare che sia il chiamante a convertire ciò che conosce, con l’inferenza attivabile per colonna anziché come euristica su tutto il file. Lo strumento da CSV a JSON rende esplicito quell’interruttore proprio per questo.

Un insieme di impostazioni predefinite che vale la pena dichiarare

Decisione Predefinito Perché
Colonne Unione di tutti i percorsi foglia, ordinata Campionare il primo oggetto scarta campi senza avvisare
Oggetti annidati Percorso puntato, separatore configurabile Le chiavi possono legittimamente contenere il separatore
Array JSON in una cella L’unico criterio reversibile. Esplodi quando l’array è il record
Contenitori vuoti [] e {} alla lettera Distinguibili da null e dalla stringa vuota
Null Cella vuota, documentato Oppure una sentinella dove la distinzione porta peso
Numeri Testo sorgente alla lettera Non farli mai passare da un float in uscita
Fine riga CRLF RFC 4180, e LF rompe più lettori di CRLF
BOM Spento, con interruttore per Excel Giusto per le pipeline, sbagliato per Excel: lascia scegliere all’utente
Caratteri di formula Con prefisso solo sul percorso Excel Il prefisso cambia il dato, quindi non deve accadere in silenzio

Non sono le uniche risposte difendibili. Il punto è che ogni convertitore prende tutte e nove le decisioni, che te lo dica o no, e solo quelli che te lo dicono meritano fiducia con un payload troppo grande per essere controllato a occhio. L’appiattitore mostra l’insieme dei percorsi prima che tu ti impegni, e la vista a tabella mostra il rettangolo che stai per ottenere.