JSON Lines e NDJSON: dati in streaming
In sintesi: Un valore JSON per riga ti regala l'appendibilità, lo streaming e il danno isolato a una sola riga; il costo è l'assenza di struttura complessiva, quindi la validazione deve avvenire riga per riga.

In sintesi: usa un array JSON quando elabori un intero batch in una volta, e JSON Lines quando ti servono append in streaming e isolamento delle righe difettose; se devi comunque validare la struttura complessiva, non passare a un formato a righe.
Una volta che i dati superano la memoria, o continuano ad arrivare, la struttura "un grande array" diventa il nemico.
1. Regole del formato
JSON Lines (.jsonl) e NDJSON (.ndjson) sono in pratica sinonimi, e le regole sono brevi in entrambi i casi:
- La codifica è UTF-8, senza BOM;
- Ogni riga contiene un valore JSON completo e valido;
- Le righe sono separate da
\n; l'ultima riga può o non può terminare con un a capo; - Nessun a capo non escapato può comparire dentro una riga — un a capo dentro una stringa deve essere scritto come
\n; - Le righe vuote di solito vengono ignorate, ma un'implementazione rigorosa dovrebbe trattarle come non valide.
{"time":"2026-10-30T10:41:45+08:00","level":"info","message":"boot"}
{"time":"2026-10-30T10:41:46+08:00","level":"warn","message":"slow query","costMs":812}
{"time":"2026-10-30T10:41:47+08:00","level":"error","message":"timeout","traceId":"a1b2"}
La sottile differenza tra i due: il sito di JSON Lines sottolinea "un valore per riga, e il valore può essere qualsiasi JSON (compresi gli scalari)", mentre la specifica NDJSON aggiunge regole di implementazione più rigorose come "niente righe vuote e niente scalari di livello superiore". In ingegneria, seguire l'insieme più rigoroso ti tiene fuori dai problemi.
2. Confronto con un array JSON
| Dimensione | Array JSON | JSON Lines |
|---|---|---|
| Aggiungere un record | Riscrivere l'intero file (cambiare la ] di chiusura) |
Aggiungere una riga |
| Lettura in streaming | Richiede un parser in streaming che tenga traccia della profondità delle parentesi | Dividere per riga |
| Una voce corrotta | L'intero file diventa non analizzabile | È interessata solo quella riga |
| Validazione dell'intero file | Validare tutto in una passata | Solo riga per riga |
| Compressione | Dimensione simile dopo gzip | Uguale, e i blocchi possono essere compressi in parallelo |
| Espressione dell'annidamento | Supportata nativamente | Ogni riga può annidarsi internamente; le righe sono indipendenti tra loro |
| Consumatori tipici | Tutti gli strumenti JSON | Sistemi di log, pipeline big-data, set di addestramento LLM |
La conclusione è diretta: se i dati sono "un lotto di record" o "un unico documento" decide quale formato usare.
3. Lettura: streaming riga per riga
In Node, l'approccio più semplice è readline:
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
/**
* Reads a JSON Lines file line by line and invokes a callback for each record.
* @param filePath Path to the file
* @param onRecord Handler for a single record
* @returns Statistics containing the number of successful and failed lines
*/
async function readJsonLines(
filePath: string,
onRecord: (record: unknown, lineNumber: number) => void,
): Promise<{ okCount: number; badCount: number }> {
const lineReader = createInterface({
input: createReadStream(filePath, { encoding: 'utf8' }),
crlfDelay: Infinity,
});
let okCount = 0;
let badCount = 0;
for await (const rawLine of lineReader) {
const line = rawLine.trim();
if (!line) {
continue;
}
try {
onRecord(JSON.parse(line), okCount + badCount + 1);
okCount += 1;
} catch {
badCount += 1;
}
}
return { okCount, badCount };
}
L'uso di memoria di questo codice dipende solo dalla dimensione di una singola riga, non dalla dimensione totale del file — è il valore più grande di un formato a righe.
3.1 Quando ti servono solo pochi campi, non chiamare prima JSON.parse
Se ogni record ha 50 campi e te ne servono 3, JSON.parse costruisce oggetti per tutti e 50.
Un parser in streaming (stream-json, clarinet) può estrarre solo i campi di destinazione, al costo di una maggiore complessità implementativa.
La regola pratica: quando un singolo record è sotto qualche centinaio di KB, chiamare direttamente JSON.parse di solito è ancora l'affare migliore.
3.2 Testo cinese e BOM
I file .jsonl generati su Windows spesso portano un BOM, e JSON.parse sulla prima riga fallirà sicuramente. Rimuovi il BOM prima di leggere, oppure ispeziona i primi tre byte mentre trasmetti il primo chunk.
4. Scrittura: append e atomicità
import { appendFile } from 'node:fs/promises';
/**
* Appends a single JSON Lines record.
* @param filePath Target file
* @param record The record object to write
* @returns A promise that resolves when the write completes
*/
async function appendRecord(filePath: string, record: unknown): Promise<void> {
const line = `${JSON.stringify(record)}\n`;
await appendFile(filePath, line, { encoding: 'utf8' });
}
Due cose da notare:
- L'output di
JSON.stringifynon contiene a capo (a meno che il\ndi una stringa non venga escapato in due caratteri), quindi una riga è sempre esattamente un record; - Quando più processi aggiungono in concorrenza, una singola
writepiù piccola di PIPE_BUF (di solito 4096 byte su Linux) è atomica; qualsiasi cosa più grande non è garantita. Per gli scenari ad alta concorrenza, passa a "un file per processo, uniti dopo il flush su disco", oppure usa uno storage con transazioni.
5. Gestione delle righe difettose
Questa è la decisione che un formato a righe ti costringe a prendere in anticipo:
| Strategia | Adatta a | Costo |
|---|---|---|
| Salta e conta | Dati che tollerano lacune, come log ed eventi analitici | I dati si perdono in silenzio, quindi l'alerting è obbligatorio |
| Fallisci subito | Dati che non devono andare persi, come fatture e pagamenti | Una riga difettosa blocca l'intero batch |
| Instrada a un file dead-letter | Casi che richiedono un backfill successivo | Storage aggiuntivo e un processo di replay |
Qualunque tu scelga, registra il numero di riga e un frammento del contenuto originale, altrimenti non potrai localizzare il problema in seguito. Il numero di riga non è presente nel messaggio di errore di JSON.parse, quindi devi mantenerlo tu.
6. Compressione e sharding
- gzip comprime
.jsonlcon un rapporto costantemente alto (i nomi dei campi si ripetono), quindi si consiglia.jsonl.gz; - gzip comprime a blocchi e supporta nativamente la decompressione in streaming, quindi non serve decomprimere l'intero file;
- suddividi i file grandi in porzioni di circa
1GB, con numeri di sequenza nei nomi, per facilitare l'elaborazione parallela e il trasferimento ripristinabile; - non preprocessare mai
.jsonl"unendolo in un array" — butta via tutti i suoi vantaggi.
7. Compromessi rispetto ad altri formati big-data
| Formato | Struttura | Schema incluso | Ideale per |
|---|---|---|---|
| JSON Lines | Indipendente per riga | No (richiede un contratto esterno) | Log, import/export, dati di addestramento |
| CSV | Tabellare | No | Report, scambio con Excel |
| Parquet | Colonnare | Sì | Query analitiche, set di dati offline molto grandi |
| Avro | Basato su righe | Sì | Messaggi Kafka, evoluzione dello schema |
Il vantaggio di JSON Lines è che qualsiasi linguaggio e qualsiasi strumento può leggerlo e scriverlo immediatamente; lo svantaggio è la mancanza di uno schema e della compressione colonnare. Una volta che i dati non stanno più su una singola macchina e servono query colonnari frequenti, è il momento di considerare Parquet.
8. Checklist
- [ ] L'estensione del file è coerente (
.jsonlo.ndjson) e mai mista; - [ ] Lo scrittore garantisce "un record per riga", con gli a capo dentro le stringhe già sottoposti a escape;
- [ ] Il lettore rimuove il BOM, salta le righe vuote e mantiene i numeri di riga;
- [ ] La strategia per le righe difettose è definita esplicitamente e monitorata (righe difettose / righe totali);
- [ ] I file grandi sono suddivisi per dimensione e abbinati a gzip e letture parallele;
- [ ] Dove esiste un contratto sui campi, viene fornito uno schema aggiuntivo e validato riga per riga.
FAQ
Come scelgo tra un array JSON e JSON Lines?
Guarda se i dati sono un lotto di record o un unico documento. Scegli JSON Lines quando i record vengono aggiunti e letti come stream: aggiungere significa scrivere una riga in fondo, e leggere richiede solo di dividere per riga. Mantieni un array JSON quando l'intera struttura deve essere validata in una passata, perché un array JSON valida tutto in una volta mentre JSON Lines può essere validato solo riga per riga.
Perché una riga corrotta non rompe l'intero file?
Ogni riga in JSON Lines è un valore JSON indipendente e completo, e il parsing è naturalmente riga per riga, quindi i dati corrotti interessano solo quella riga. In un array JSON, un singolo errore di sintassi rende l'intero file non analizzabile, ed è esattamente per questo che i dati a righe si adattano ai log e agli eventi analitici.
Devo convertire JSON Lines in un array JSON?
Puoi farlo: leggi riga per riga, chiama JSON.parse su ogni record e raccoglili in un array. Farlo richiede che l'intero contenuto stia in memoria e rinuncia alle letture in streaming e agli append di una riga. Preprocessare .jsonl in un array butta via tutti i vantaggi del formato, quindi è ragionevole solo per una consegna una tantum in cui i dati stanno in memoria.