JSONL in pratica: aprire, convertire e diagnosticare le righe difettose
In sintesi: Passare un intero file .jsonl a JSON.parse e ricevere un errore quasi mai significa che il file è rotto — significa che la strategia di lettura è sbagliata. Questa guida copre i modi standard di ispezionare e leggere JSONL riga per riga, i comandi per convertire da e verso array JSON, una tabella di sei tipi di righe difettose e il modo corretto di consumare NDJSON da risposte HTTP in streaming.

In sintesi: ogni riga di un file JSONL è un valore JSON valido e indipendente, ma il file nel suo complesso non è un unico documento JSON. Se passare l'intero file a JSON.parse o json.load fallisce, nove volte su dieci il file sta bene — è la strategia di lettura a essere sbagliata.
Un record per riga, niente virgole tra le righe, niente array esterno — JSONL rinuncia alla "struttura globale del documento" in cambio di scrittura per accodamento, lettura in streaming e isolamento dei guasti riga per riga. Tutti e tre i vantaggi si realizzano solo elaborando il file riga per riga, e tutti e tre diventano errori nel momento in cui si prova a fare il parse dell'intero file in una volta.
1. Tre nomi, un formato: JSONL, NDJSON, JSON Lines
I principianti spesso assumono che la differenza di nomi implichi una differenza di sintassi. Non è così:
| Nome | Contesto tipico | Estensione / media type |
|---|---|---|
| JSON Lines | Ingegneria dei dati, dataset ML (pandas, Spark, Hugging Face) | .jsonl |
| NDJSON | API HTTP in streaming, dati in tempo reale | .ndjson, application/x-ndjson |
| JSONL | Abbreviazione informale per l'uno o l'altro | — |
Qualsiasi strumento che legge uno legge anche l'altro. L'unico criterio di scelta del nome è la convenzione del tuo ecosistema a valle: scrivi .jsonl quando prepari dataset per Hugging Face, imposta application/x-ndjson quando esponi un'API in streaming. Un file JSONL portabile soddisfa tre regole: codifica UTF-8 (senza BOM), un valore JSON completo per riga, righe separate da \n. Un newline finale a fine file è fortemente raccomandato, e le righe vuote non sono record validi. I dettagli del protocollo e la motivazione del design sono trattati nella nostra panoramica di JSON Lines e NDJSON; questo articolo riguarda strettamente cosa fare una volta che il file è tra le mani.
2. Ispezione: prima head, poi grep, l'editor per ultimo
Fare doppio clic su un .jsonl da 500 MB per aprirlo in un editor è l'errore più comune: tre milioni di righe di testo più evidenziazione della sintassi più un buffer di undo congeleranno praticamente qualsiasi editor — e non è colpa del JSON. Fare il parse di un documento da 45 MB richiede circa 208 ms in Node 22; a bloccarsi è il rendering. I formati orientati alle righe esistono per gli strumenti a riga, perché i record coincidono con le righe fisiche:
# Prima misura la scala: numero di record e dimensione del file, senza leggere il contenuto
wc -l events.jsonl && du -h events.jsonl
# Guarda come è fatto un record (conferma campi e struttura)
head -n 1 events.jsonl | jq .
# Trova i record target direttamente: gli strumenti a riga funzionano — un privilegio del JSONL
grep '"level":"error"' events.jsonl | head -n 5
tail -n 100 events.jsonl | jq 'select(.latency_ms > 500)'
Nota l'ultimo comando: ogni riga in uscita da tail è ancora un valore JSON indipendente, quindi va direttamente in jq senza dover ricostruire un array. Questo approccio funziona identicamente su file di diversi gigabyte con overhead di memoria quasi nullo — è lo stesso principio di perché i file JSON grandi rallentano le pagine.
3. I tre modelli standard di lettura riga per riga
La lettura riga per riga deve gestire tre cose: saltare le righe vuote (specialmente il newline finale), segnalare gli errori con il numero di riga e non lasciare mai che una sola riga difettosa faccia fallire tutto il lavoro. I modelli standard delle tre lingue ruotano attorno a questi punti.
Python, solo libreria standard:
import json
with open("events.jsonl", encoding="utf-8") as f:
for line_number, line in enumerate(f, start=1):
if not line.strip(): # salta le righe vuote
continue
try:
record = json.loads(line) # analizza ogni riga indipendentemente
except json.JSONDecodeError as e:
print(f"Riga {line_number} fallita: {e}") # segnala, non interrompere
continue # isola la riga difettosa
handle(record)
Node con il modulo readline integrato, memoria costante a prescindere dalla dimensione del file:
import fs from "node:fs";
import readline from "node:readline";
const input = fs.createReadStream("events.jsonl", "utf8");
const lines = readline.createInterface({ input, crlfDelay: Infinity });
let lineNumber = 0;
for await (const line of lines) {
lineNumber += 1;
if (!line.trim()) continue;
try {
handle(JSON.parse(line));
} catch (e) {
console.error(`Riga ${lineNumber}: ${e.message}`);
}
}
In Go 1.27 encoding/json/v2 diventa lo standard stabile, e il suo decodificatore in stream valore per valore si adatta perfettamente al JSONL; usa gojq se devi incorporare espressioni jq. Sulle librerie, non avere fretta di installare pacchetti: jsonlines in Python riporta il numero e il testo grezzo di ogni riga che fallisce, e orjsonl legge e scrive direttamente file compressi gzip/bzip2/zstd; il pacchetto Node ndjson non pubblica rilasci dal 2020 — il readline integrato basta.
4. Convertire da e verso array JSON
La direzione conta; invertila e ogni comando sarà sbagliato. Ricorda una regola: array → JSONL è una map (ogni record compresso su una riga), JSONL → array è una collect (righe raccolte in una lista).
# Array JSON → JSONL: -c comprime ogni elemento su una riga
jq -c '.[]' records.json > records.jsonl
# JSONL → array JSON: -s raccoglie tutto l'input in un unico array
jq -s '.' events.jsonl > records.json
# Filtra JSONL riga per riga: non serve -s, jq processa l'input come stream
jq -c 'select(.active == true)' events.jsonl > active.jsonl
I one-liner Python sono equivalenti: array → JSONL scrive json.dumps(r, ensure_ascii=False) + "\n" per record; al contrario si raccolgono i json.loads riga per riga in una lista. Ogni direzione ha una trappola. All'andata (array → JSONL), ogni newline reale dentro un record deve essere escapato da json.dumps — altrimenti un record si strappa su più righe. Al ritorno (JSONL → array), non concatenare le righe in un'unica stringa gigante per analizzarla — con milioni di righe è una copia integrale non necessaria in memoria. Le aggregazioni (somme, raggruppamenti, medie) richiedono -s, che carica tutto; su file di diversi GB passa ad awk o a un accumulatore in streaming in Python.
5. Sei tipi di righe difettose e come diagnosticarle
Quasi tutti gli errori JSONL vivono a livello di riga. Validare riga per riga riportando i numeri di riga è l'unica diagnosi affidabile. Ordinate per frequenza:
| Sintomo | Causa radice | Correzione |
|---|---|---|
Extra data a fine riga |
Virgole aggiunte tra le righe | Eliminale; ogni riga deve bastare a sé |
| Un record diviso su più righe | Newline reale non escapato dentro il record (formattato a mano) | Ricomprimi con json.dumps / jq -c |
Prima riga fallisce con Unexpected token ï |
Il file ha un BOM | Risalva in UTF-8 senza BOM |
| Solo l'ultima riga dà errore ma sembra corretta | Newline finale mancante o spazi indesiderati | Aggiungi \n; salta le righe vuote per convenzione |
| Errore di newline dentro una stringa | Newline letterale dentro una stringa, non \n |
Correggi il generatore; non concatenare mai stringhe JSON a mano |
| Interi grandi cambiano valore | Parse via double (troncamento a 2^53) | Usa un parser che preserva la precisione, vedi precisione bigint |
La routine diagnostica è sempre di tre passi: head -n 1 per confermare codifica e struttura della prima riga; eseguire uno script riga per riga che riporta il numero di ogni riga difettosa invece di fermarsi alla prima; poi estrarre le righe difettose una a una — una riga è un record completo, ispezionarla in isolamento non costa nulla. Per gli import in batch la politica raccomandata è "quarantena, non aborto": scrivi le righe difettose con i loro numeri in errors.jsonl, continua a caricare quelle buone, poi decidi il reimport in base alla proporzione di righe difettose.
6. JSONL su HTTP: consumo riga per riga e backpressure
Lo schema tipico di NDJSON su HTTP è un server che scrive di continuo e un client che consuma di continuo. Sul client, non chiamare response.json() — bufferizza l'intero corpo prima del parse, vanificando il senso dello streaming. Leggi lo stream del corpo direttamente, dividi su \n e riporta qualsiasi frammento finale incompleto all'iterazione successiva:
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const lines = buffer.split("\n");
buffer = lines.pop(); // l'ultimo pezzo può essere una riga incompleta
for (const line of lines) {
if (line.trim()) handle(JSON.parse(line));
}
}
Sul server due cose sono obbligatorie: prima, quando res.write() restituisce false, attendi l'evento drain (backpressure), altrimenti la memoria cresce senza limiti ogni volta che il consumatore è più lento del produttore; seconda, disabilita il middleware gzip sulle rotte in streaming — gzip deve bufferizzare l'intera risposta prima di comprimere qualcosa, il che neutralizza completamente lo streaming NDJSON. Entrambi i comportamenti sono confermati da test locali e da più fonti indipendenti in inglese, non sono supposizioni.
7. Quando JSONL è la scelta sbagliata
Anche lo spazio negativo della scelta del formato conta. Tre scenari dovrebbero restare con l'array JSON:
| Requisito | Perché JSONL mal si adatta | Usa invece |
|---|---|---|
| Validare la struttura dell'intero documento | Nessuna struttura globale; la deriva dei campi passa inosservata | Array JSON + Schema |
| Accesso casuale al record N | Lunghezze di riga variabili; nessuna ricerca binaria | SQLite / Parquet |
| Annidamento profondo, riferimenti tra record | Ogni riga è isolata; i riferimenti non hanno dove stare | Documento JSON standard |
Regola empirica in una frase: se i record arrivano nel tempo o vengono consumati riga per riga (log, eventi, export, dataset di addestramento), usa JSONL; se un piccolo volume viene prodotto una volta, consumato una volta e richiede la semantica del documento intero, usa un array JSON. Le varianti derivate (JSON Text Sequences delimitate da RS) sono un altro argomento — vedi l'articolo su RFC 7464.
FAQ
C'è una differenza reale tra JSONL e NDJSON?
In termini ingegneristici no. Entrambi significano "un valore JSON completo per riga, nessun wrapper esterno, niente virgole tra le righe", e qualsiasi strumento che ne supporta uno supporta anche l'altro. La differenza è di denominazione e abitudini dell'ecosistema: .jsonl domina l'ingegneria dei dati e i dataset ML; .ndjson e application/x-ndjson dominano le API HTTP in streaming. Quando ti integri con un sistema esterno, segui l'estensione e il media type che la sua documentazione specifica.
Posso leggere un file .jsonl direttamente con JSON.parse?
Non il file intero. JSON.parse accetta esattamente un testo JSON, mentre JSONL è una serie di valori indipendenti disposti riga per riga, quindi il parse dell'intero file fallisce sempre con "Unexpected token" o "Extra data". Dividi in righe e analizza ogni riga non vuota singolarmente, riportando il numero di riga in caso di fallimento.
Se una riga di un file JSONL è corrotta, l'intero file è compromesso?
No — l'isolamento dei guasti riga per riga è il cuore del design del formato. Una riga difettosa danneggia solo sé stessa: saltala, annota il suo numero e continua la lettura. Per gli import in batch, metti in quarantena le righe difettose in un file separato, carica i record corretti normalmente, poi decidi correzione e reimport in base alla proporzione di righe difettose.
Come converto JSONL in Excel o CSV?
Prima trasformalo in array o estrai i campi riga per riga, poi esegui una conversione tabellare: comprimi con jq -c '.[]' data.jsonl e passa il risultato a un convertitore, o invia ogni riga tramite json.loads a pandas. Attenzione: i campi annidati appiattiti producono nomi di colonne con punti, i campi array possono essere serializzati solo come testo dentro la cella, e le colonne di ID lunghi andrebbero dichiarate come stringhe in anticipo per evitare perdite di precisione.