JSON in CSV: da dove viene l'intestazione e perché i valori cambiano
In sintesi: Nove esperimenti su un convertitore reale: la strategia "chiavi della prima riga" perde campi in silenzio, null, assenza e stringa vuota diventano tutti celle vuote, un intero a 20 cifre perde precisione già in JSON.parse, e un punto in una chiave fa collidere due percorsi diversi nella stessa colonna. Come si costruisce l'intestazione, quali valori si deformano e come scegliere una strategia in base all'uso a valle.

Quando una conversione da JSON a CSV perde dati, quasi mai accade durante la fase di "conversione". La perdita avviene in due domande che nessuno si è posto: chi ha deciso l'intestazione, e ogni cella resta riconoscibile come il valore che era?
Un campo scompare in trenta secondi
Molti convertitori — compresa l'idiomatica pipeline jq — prendono l'intestazione solo dal primo record. Riproduci le conseguenze in 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 del secondo record scompare senza errori né avvisi
La chiave email non è nell'intestazione: non ha nemmeno il diritto di diventare una cella vuota — la colonna non è mai stata creata, e il valore viene gettato via in silenzio. La correzione è l'unione delle chiavi: attraversare tutti i record e creare una colonna per ogni chiave che compare in qualche punto. Gli stessi dati passati per l'unione producono l'intestazione id,name,email, con una cella vuota dove manca name — e nulla si perde.
Il convertitore diretto della nostra pagina /convert/csv applica esattamente questa strategia. Con l'input [{"id":1,"name":"A"},{"id":2,"email":"b@x.com"}], produce:
id,name,email
1,A,
2,,b@x.com
Le chiavi sono ordinate per prima comparsa e i campi assenti diventano celle vuote. jsonoperations e jsonbeam chiamano entrambe questa "la correzione del bug classico" — ma "usa l'unione" è solo metà della storia. L'unione ha il suo prezzo; è la sezione successiva.
Tre modi di costruire un'intestazione — e a chi conviene ciascuno
| Strategia di intestazione | Come si costruisce | Forza | Costo |
|---|---|---|---|
| Chiavi della prima riga | Si legge solo il primo record | Poche colonne, stabile, una riga di jq | I nuovi campi dei record successivi vanno persi in silenzio |
| Unione delle chiavi | Si leggono tutti i record | Nessun campo perso | Colonne scarse e molti vuoti quando le forme variano |
| Lista esplicita di colonne | Definita a mano o da schema | Completamente controllabile, riproducibile | Va mantenuta; i campi nuovi richiedono aggiornamento manuale |
Il criterio di scelta non è "quale è più sofisticato" ma cosa il destinatario fa di questo CSV:
- Un collega che ci sbircia dentro: le chiavi della prima riga bastano, le forme dei record sono di solito uniformi.
- Un report o una pipeline di dati: l'unione delle chiavi è il minimo sindacale — una cella vuota è essa stessa informazione.
- Un export automatico ricorrente: la lista esplicita di colonne. La deriva di forma (campo aggiunto a monte, chiave rinominata) deve fallire rumorosamente, non produrre in silenzio una tabella che nessuno nota essere sbagliata.
La terza opzione è la più trascurata. Nell'automazione, "la conversione è riuscita" e "la conversione è corretta" sono due affermazioni diverse — e la lista esplicita di colonne è l'unico meccanismo che le separa.
Quando una chiave contiene un punto, i percorsi di appiattimento collidono
L'appiattimento in notazione puntata trasforma {"a": {"b": 2}} nella colonna a.b. Ma se la chiave stessa contiene un punto? Testato con pandas:
import pandas as pd
d = [{"a.b": 1, "a": {"b": 2}}]
pd.json_normalize(d, sep=".")
# Sopravvive una sola colonna: ['a.b'] — due chiavi completamente diverse collidono sullo stesso percorso
La chiave letterale a.b e il percorso annidato a → b producono lo stesso nome di colonna, uno sovrascrive l'altro, e i dati si corrompono in silenzio. È un esempio costruito, ma capita con dati reali: i nomi delle metriche di monitoraggio (cpu.usage) e i campi dei log (error.type) adorano i punti.
Due rimedi:
- Cambiare il separatore.
sep="_"o un separatore personalizzato, dopo aver verificato che le chiavi originali non lo contengano mai. - Cambiare la notazione dei percorsi. JSON Pointer usa
/come separatore (/a/b); il convertitore Excel di toolvore segue questa strada. Ha la stessa classe di ambiguità — una chiave con/va scritta come~1— ma le regole di escape sono normalizzate nella RFC 6901 invece che inventate da ogni strumento.
Qualunque scelta faccia, scriva la regola nella documentazione dell'export. L'intestazione CSV è l'unico "schema" che questi dati portano; il lettore non trova le tue regole di appiattimento da nessun'altra parte.
Gli array: tre strategie, ognuna perde qualcosa di diverso
Gli array sono il vero ostacolo del JSON verso CSV: una cella non può contenere più valori. Tre strategie, con perdite verificate in pratica:
| Strategia | Forma di uscita | Cosa si perde | Ideale per |
|---|---|---|---|
| JSON-nella-cella | L'array serializzato come stringa JSON nella cella | Gli strumenti tabellari non possono filtrare né aggregare gli elementi | Fedeltà prima di tutto, ispezione manuale |
| Colonne indicizzate | tags.0, tags.1, una colonna ciascuna |
Il numero di colonne esplode con l'array più lungo; molti vuoti | Piccoli array di lunghezza fissa |
| Righe esplose | Una riga per elemento, campi padre duplicati | Inflazione di righe; il comportamento degli array vuoti va definito a parte | Analisi di dettaglio, aggregazione per elemento |
I nostri convertitori /convert/csv e /convert/table usano JSON-nella-cella. L'input [{"orderId":"A001","items":[{"sku":"X1","qty":2},{"sku":"Y9","qty":1}]}] produce:
orderId,items
A001,"[{""sku"":""X1"",""qty"":2},{""sku"":""Y9"",""qty"":1}]"
Quando servono righe esplose, il record_path di pandas lo fa direttamente:
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=".")
# Due righe di dettaglio; orderId e total duplicati su ciascuna
Nell'esplosione ci sono tre trappole da decidere in anticipo: esplodere più array insieme costruisce un prodotto cartesiano; gli array vuoti non producono alcuna riga per default (la tua lista clienti perde persone); e i campi padre duplicati richiedono deduplicazione quando aggregi per orderId a valle. La guida di convertcsvonline elenca "decidere prima la granularità" come primo passo esattamente per questo — fissata la granularità, le tre domande hanno ciascuna una sola risposta.
Testato: valori che escono cambiati dalla conversione
Un test di fedeltà eseguito sul nostro convertitore reale (Node v22.13.1 + papaparse, la stessa implementazione di /convert/csv):
1. null, assenza e stringa vuota diventano tutti celle vuote. Input [{"a":null},{"b":""},{"c":1}] — tutte e tre le celle escono vuote. Il CSV non ha tre stati, solo "ci sono caratteri" e "non ci sono caratteri". Dopo un round-trip, tutti e tre si leggono come ""; la semantica originale è irrecuperabile.
2. Oggetti e array annidati diventano stringhe JSON. Le virgolette vengono raddoppiate secondo la RFC 4180 ({""name"":""张三""}), e incollando la cella in un qualsiasi parser JSON il valore torna — ecco perché JSON-nella-cella preserva la fedeltà.
3. Un intero a 20 cifre perde precisione già al parse — e la colpa non è del CSV. Input "id": 9007199254740993; nel CSV c'è già 9007199254740992. JSON.parse in JavaScript usa double IEEE 754: la perdita avviene durante il parsing, prima di qualsiasi convertitore CSV. Il json.load di Python la preserva integralmente. I campi tipo identificatore (ID snowflake, numeri d'ordine) dovrebbero arrivare come stringhe fin dal produttore. Approfondisci la precisione numerica in perché i numeri grandi perdono precisione.
4. Gli zeri iniziali nelle stringhe sono al sicuro al livello testo del CSV. "phone": "00123" resta 00123 nel CSV, perché è una stringa dall'inizio alla fine e nulla la scalfisce. A scalfire gli zeri è il passaggio successivo, in cui Excel tratta la cella come numero — vedi quattro modi per convertire JSON in Excel.
5. Campi con virgole, virgolette o a-capo. papaparse segue la RFC 4180: citare il campo, raddoppiare le virgolette interne. "He said \"hi\", ok" esce come "He said ""hi"", ok" e sopravvive intatto al round-trip. I serializzatori scritti a mano che aggirano una libreria falliscono quasi sempre proprio lì.
Round-trip: cosa cambia l'interruttore dell'inferenza
CSV → JSON deve indovinare un tipo per ogni cella. Il nostro convertitore disattiva l'inferenza di tipo per impostazione predefinita e restituisce tutto come stringa:
[{ "id": "9007199254740992", "phone": "00123" }]
Con typeInference attivo, id diventa un numero (la precisione è già persa), mentre phone resta stringa — le regole di inferenza escludono le forme con zero iniziale come 010, che non sono letterali numerici legali, così la stringa sopravvive per fortuna.
Memorizza questa asimmetria: l'inferenza spenta nel peggiore dei casi ti costa i tipi; l'inferenza accesa può costarti dati. Le colonne che hanno bisogno di numeri vanno convertite con definizioni esplicite di colonna, non affidate a un'inferenza globale. L'implementazione rowsToRecords del nostro articolo precedente JSON, CSV e dati tabellari è esattamente questo principio in codice.
Scegliere la strategia in base al destinatario: una tabella decisionale
| Chi riceve il CSV | Strategia di intestazione | Gestione degli array | Tipi |
|---|---|---|---|
| Un collega che lo apre una volta | Unione delle chiavi | JSON-nella-cella | Senza inferenza |
| Excel / reportistica | Unione delle chiavi | JSON-nella-cella, o colonne indicizzate se lunghezza fissa | Colonne identificatore forzate come testo |
| Import in database | Lista esplicita di colonne | Tabelle master/dettaglio separate, o esplosione | Definizioni esplicite di colonna |
| BI / aggregazione | Lista esplicita di colonne | Righe esplose, granularità = più piccola unità di analisi | Definizioni esplicite di colonna |
| Pipeline automatizzata | Lista esplicita di colonne | JSON-nella-cella, esplosione a valle | Inferenza disattivata, conversione colonna per colonna |
La legge comune: più sei vicino all'umano, più puoi permetterti di rilassare; più sei vicino alla macchina, più tutto deve essere esplicito.
Una checklist da copiare
- [ ] L'intestazione viene da un'unione completa delle chiavi o da una lista esplicita? In caso negativo, puoi dimostrare che nessun record ha perso un campo?
- [ ] Le chiavi possono contenere punti o barre? La collisione con il separatore di appiattimento è stata verificata?
- [ ] La strategia degli array è documentata? Il comportamento degli array vuoti è deciso?
- [ ] Per
null, assenza e stringa vuota: cosa legge il destinatario per ciascuno? Scegli una regola e congélala. - [ ] Le colonne identificatore (ID, numeri d'ordine, telefoni) viaggiano come stringhe da capo a fine?
- [ ] Dopo l'esplosione, il numero di righe è uguale alla somma delle lunghezze degli array? Verificato a campione?
- [ ] I campi di esempio con virgole, virgolette e a-capo sopravvivono intatti a un round-trip?
- [ ] L'inferenza globale di tipo è disattivata al reimport, con definizioni esplicite per le colonne numeriche?
Solo quando tutte e otto hanno una risposta netta, questo CSV è "corretto" — e non semplicemente "convertito con successo".
FAQ
Perché il convertitore non appiattisce gli oggetti annidati in colonne per impostazione predefinita?
L'appiattimento fa uscire di controllo il numero di colonne: l'annidamento profondo produce nomi come user.address.geo.lat, e combinato con l'unione delle chiavi, un solo record di forma diversa aggiunge una pila di colonne. Le stringhe JSON nelle celle preservano la fedeltà e un numero stabile di colonne; appiattisci esplicitamente quando ti serve.
Cosa fare se l'intestazione contiene nomi di colonna duplicati?
La collisione di due chiavi diverse dopo l'appiattimento (come a.b contro l'annidato a→b) va risolta a monte: cambia separatore o passa a JSON Pointer. Per i duplicati dentro lo stesso oggetto, i convertitori in genere tengono l'ultimo valore; i duplicati nell'intestazione dipendono dall'implementazione — l'unparse di papaparse non deduplica, ed Excel fonde o scarta i duplicati in silenzio all'apertura. Ecco perché è nella checklist.
I campi padre duplicati dopo l'esplosione sono un bug?
No — è design. Una riga esplosa è una riga di dettaglio, e una riga di dettaglio deve poter essere ricollegata al record padre tramite chiave. Aggrega a valle per la chiave padre; se la dimensione preoccupa, esporta il file di dettaglio e una tabella master separata.
Come verificare che la mia conversione non abbia perso alcun campo?
Fai un audit delle chiavi: calcola l'unione di tutte le chiavi del JSON sorgente, poi l'insieme delle intestazioni di colonna del CSV, e fanne la differenza. Una differenza non vuota significa campi persi. Nelle pipeline a lotti, codificala come asserzione nei tuoi test: qualsiasi cambio di convertitore che cominci a perdere campi fallirà automaticamente.