JSON Toolkit

Conversione tra JSON e CSV e dati tabellari

In sintesi: CSV è bidimensionale, JSON ha forma ad albero. La parte difficile della conversione non è il codice — è decidere in anticipo come appiattire gli array e come preservare i tipi.

Principiante~10 min di letturaPubblicato il 2026-10-30Aggiornato il 2026-10-30
Copertina della conversione JSON ↔ CSV e dati tabellari

In sintesi: appiattisci con chiavi a punti e array serializzati come stringa JSON quando saranno persone a leggere l'esportazione, separa in una tabella padre più tabelle correlate quando carichi un database, ed espandi gli array in righe aggiuntive solo per i pivot; in fase di ripristino, converti con definizioni esplicite delle colonne invece che con l'inferenza globale.

Appiattire un albero in una tabella perde sempre la gerarchia; ripristinare una tabella in un albero indovina sempre i tipi. Entrambe le direzioni richiedono che le regole siano dichiarate in anticipo.

1. Cosa specifica davvero CSV

RFC 4180 occupa solo poche pagine e specifica meno di quanto molti presumano:

  1. Ogni record occupa una riga, con i campi separati da virgole;
  2. Il terminatore di riga è CRLF, anche se in pratica \n è accettato;
  3. Un campo che contiene una virgola, una virgoletta doppia o un a capo deve essere racchiuso tra virgolette doppie;
  4. Le virgolette doppie dentro un campo si scrivono come due virgolette doppie "";
  5. La prima riga può essere un'intestazione;
  6. Non è specificata alcuna codifica dei caratteri, né i tipi.
id,name,remark
1,张三,"喜欢 JSON,讨厌尾逗号"
2,李四,"他说:""这行有引号"""
3,王五,"多行
备注"

Fai attenzione alla regola 3: i campi che contengono a capo sono legali, quindi un parser ingenuo che divide su \n e poi su , è destinato a sbagliare.

2. JSON → CSV: decidi prima la struttura

Esistono solo tre strategie per appiattire una struttura ad albero in una tabella bidimensionale:

Strategia 1: appiattire i campi (la più comune)

{ "id": 1, "user": { "name": "张三", "city": "杭州" }, "tags": ["a", "b"] }
id,user.name,user.city,tags
1,张三,杭州,"[""a"",""b""]"
  • Unisci le chiavi degli oggetti annidati con .;
  • Gli array non possono essere espressi come colonne, quindi serializzali con JSON.stringify direttamente nella cella (che resta un campo CSV legale);
  • Svantaggio: i fogli di calcolo non possono filtrare l'array, quindi serve un secondo parsing.

Strategia 2: espandere l'array in più righe

id,user.name,tag
1,张三,a
1,张三,b

Si adatta alle relazioni uno-a-molti in cui i consumatori a valle aggregano per elemento. Lo svantaggio sono i campi padre duplicati e un numero di righe che esplode.

Strategia 3: separare in più tabelle

Una tabella principale più tabelle correlate unite da un id — l'approccio più pulito, ma va oltre "una singola esportazione" e assomiglia più alla modellazione dei dati.

Quale scegliere? La strategia 1 per esportazioni destinate alle persone, la 3 per caricare in un database e la 2 per fare pivot sui dati.

3. CSV → JSON: come vengono indovinati i tipi

CSV non ha tipi, quindi ogni passaggio del ripristino è un'ipotesi:

Contenuto della cella Possibile significato Rischio di convertire direttamente in numero
00123 Matricola dipendente, codice postale Zeri iniziali persi → 123
1.10 Numero di versione, importo Diventa 1.1
1e5 Identificatore o notazione scientifica Diventa 100000
2026-10-30 Data o stringa Ambiguità su fuso orario e formato
13800138000 Numero di telefono Ancora sicuro con 11 cifre, ma alcuni ID perdono precisione
"" Stringa vuota o null Ambiguità semantica

Valore predefinito consigliato: tratta tutto come stringa, e lascia che uno schema o una definizione esplicita delle colonne decida quali colonne diventano numeri o booleani. L'"inferenza intelligente" è deliziosa in esportazione e una fonte di incidenti in importazione.

type ColumnType = 'string' | 'number' | 'boolean' | 'json';

/**
 * Converts CSV rows into objects according to the column definitions.
 * @param rows The raw row array; the first row is the header
 * @param columnTypes Mapping from column name to type
 * @returns The converted array of objects
 */
function rowsToRecords(
  rows: string[][],
  columnTypes: Record<string, ColumnType>,
): Array<Record<string, unknown>> {
  const [headerRow, ...dataRows] = rows;

  return dataRows.map((dataRow) => {
    const record: Record<string, unknown> = {};

    headerRow.forEach((columnName, columnIndex) => {
      const rawValue = dataRow[columnIndex] ?? '';
      const columnType = columnTypes[columnName] ?? 'string';

      if (columnType === 'number') {
        const numericValue = Number(rawValue);
        record[columnName] = Number.isFinite(numericValue) ? numericValue : null;
        return;
      }
      if (columnType === 'boolean') {
        record[columnName] = rawValue === 'true' || rawValue === '1';
        return;
      }
      if (columnType === 'json') {
        try {
          record[columnName] = JSON.parse(rawValue);
        } catch {
          record[columnName] = rawValue;
        }
        return;
      }
      record[columnName] = rawValue;
    });

    return record;
  });
}

4. Tre trappole intorno a Excel

4.1 Codifica

Quando Excel apre un CSV UTF-8 senza BOM, il testo cinese diventa mojibake. Aggiungere un BOM (\uFEFF) in esportazione è la correzione di compatibilità più economica:

const bom = '\uFEFF';
const csvContent = `${bom}${headerLine}\n${bodyLines.join('\n')}`;

4.2 Numeri lunghi che diventano notazione scientifica

Un codice fiscale o un documento di identità di 18 cifre o un numero d'ordine di 19 cifre appare in Excel come 1.23457E+17, e il valore stesso è già stato troncato. Prefissare il campo con un tab o racchiuderlo come formula (="1234...") preserva la formattazione del testo, ma trasforma la cella in una formula. L'approccio più sicuro è dire agli utenti di "usare la funzione di importazione e specificare la colonna come testo", oppure offrire un .xlsx insieme all'esportazione.

4.3 Delimitatori influenzati dalla locale

Alcune locale europee impostano il punto e virgola ; come separatore predefinito. Quando distribuisci tra regioni diverse, fissa il CSV sulla virgola e dichiaralo, oppure offri una versione TSV (separata da tab) — TSV è più tollerante alle locale.

5. Gestione di file grandi

Non caricare in memoria in un colpo solo CSV con più di centomila righe:

import { createReadStream } from 'node:fs';
import { parse } from 'csv-parse';

const parser = createReadStream('orders.csv').pipe(
  parse({ columns: true, skipEmptyLines: true, bom: true }),
);

for await (const record of parser) {
  // Process one record at a time; memory usage is independent of file size
}

La direzione di esportazione funziona allo stesso modo: genera e scrivi in streaming man mano, invece di concatenare un'unica stringa gigantesca.

6. Autoverifica della coerenza del round-trip

Un round trip CSV (JSON → CSV → JSON) non è quasi mai esattamente identico, ma puoi definire quali perdite sono accettabili:

Controllo Aspettativa
Numero di campi Identico
Valori stringa Identici (compresi gli zeri iniziali)
Tipi numerici Determinati dalla definizione delle colonne, non dall'inferenza
Oggetti annidati Recuperabili tramite percorsi a punti
Array Visibili dopo la serializzazione a stringa JSON
Valori vuoti Normalizzati a null o alla stringa vuota — scegli esattamente uno

Mettere questa tabella nei test è molto più efficace di un accordo verbale.

7. Checklist

  • [ ] La semantica "una riga è un record" è dichiarata esplicitamente e riflessa nel nome del file esportato;
  • [ ] Ogni colonna che può perdere precisione viene esportata come stringa;
  • [ ] I CSV con testo cinese includono un BOM, oppure la codifica è dichiarata esplicitamente;
  • [ ] I campi che contengono virgole, virgolette o a capo sono sottoposti a escape correttamente (usa una libreria, non concatenare stringhe da solo);
  • [ ] I file grandi passano attraverso letture e scritture in streaming;
  • [ ] Le importazioni convertono i tipi per definizione di colonna, mai tramite inferenza intelligente globale.

FAQ

Come scelgo tra appiattimento, espansione delle righe e separazione delle tabelle?

Appiattisci per le esportazioni destinate alle persone: unisci le chiavi annidate con . e serializza gli array con JSON.stringify nella cella, al costo di non poter filtrare quegli array in un foglio di calcolo. Separa in una tabella padre più tabelle correlate quando la destinazione è un database. Espandi gli array in righe aggiuntive quando i consumatori a valle aggregano per elemento, accettando campi padre duplicati.

Perché un'importazione CSV non dovrebbe inferire i tipi automaticamente?

RFC 4180 specifica delimitatori ed escape ma non i tipi, quindi ogni passaggio di ripristino è un'ipotesi: 00123 perde gli zeri iniziali, 1.10 diventa 1.1 e 1e5 diventa 100000. Tratta tutto come stringa per impostazione predefinita e lascia che uno schema o una definizione esplicita delle colonne decida quali colonne diventano numeri o booleani.

Devo aspettarmi che un round trip CSV riproduca esattamente il JSON originale?

Quasi mai. Definisci invece le perdite accettabili: numero di campi identico, valori stringa identici compresi gli zeri iniziali, tipi numerici decisi dalla definizione delle colonne, oggetti annidati recuperabili tramite percorsi a punti, array visibili dopo la serializzazione e valori vuoti normalizzati a null o alla stringa vuota — scegli esattamente uno.