JSON Toolkit

JSON Text Sequences (RFC 7464): JSON in streaming delimitato da RS

In sintesi: RFC 7464 usa un separatore 0x1E per trasmettere testi JSON che si possono leggere e scrivere contemporaneamente. Copre le regole, le differenze con NDJSON e un'implementazione split-then-parse.

Intermedio~9 min di letturaPubblicato il 2026-10-31Aggiornato il 2026-10-31
Copertina di JSON Text Sequences e del record separator RFC 7464

In sintesi: usa JSON-seq quando controlli entrambe le estremità, ti serve un media type registrato e vuoi che il lettore si risincronizzi dopo un troncamento; usa NDJSON quando i dati alimentano sistemi di log, devono essere leggibili da persone o devono funzionare con gli strumenti esistenti — non imporre mai JSON-seq a un interlocutore che capisce solo input a righe.

NDJSON divide i record sugli a capo, ma un a capo è una convenzione umana. RFC 7464 scrive il confine nel flusso di byte stesso: un invisibile 0x1E prima di ogni record.

1. Il problema risolto da un singolo byte

RFC 7464 è stato pubblicato a febbraio 2015 con il titolo JSON Text Sequences, e registra il media type application/json-seq. Non aggiunge nuovi tipi di dati. Definisce solo come una sequenza di testi JSON è disposta dentro un unico flusso di byte. L'intera regola sta in una frase: metti un RS prima di ogni testo JSON e un LF dopo.

  • RS è il record separator ASCII, code point 0x1E, noto in Unicode come INFORMATION SEPARATOR TWO (U+001E);
  • LF è il line feed, 0x0A.

Due record appaiono così a livello di byte:

1E 7B 22 69 64 22 3A 31 7D 0A      ← 0x1E {"id":1} + LF
1E 7B 22 69 64 22 3A 32 7D 0A      ← 0x1E {"id":2} + LF

L'RS si trova prima del record, che è l'opposto della disposizione di NDJSON "scrivi il contenuto, poi scrivi il delimitatore". Quella direzione cambia il comportamento di recupero: un lettore che vede 0x1E sa che il record precedente è terminato, quindi anche un record troncato a metà scrittura non può sfumare il confine del successivo.

Non è un design teorico. L'RFC menziona esplicitamente i record aggiunti ai file di log che vengono troncati dal filesystem dopo un crash o un kill amministrativo, e afferma che un parser non dovrebbe interrompere l'intera lettura per questo.

2. Due insiemi di regole: encoder e parser

RFC 7464 fornisce insolitamente due grammatiche ABNF, una che vincola gli scrittori e una che rilassa i lettori.

Ruolo Grammatica Significato
Encoder *(RS JSON-text LF) Un RS prima di ogni testo JSON, un LF dopo
Parser *(1*RS possible-JSON) Sono ammessi RS consecutivi, e l'LF tra i record è opzionale

In termini ingegneristici, ci sono cinque regole ferree:

  1. UTF-8 è obbligatorio; UTF-16 e UTF-32 non devono essere usati;
  2. Il parser tratta "qualsiasi byte diverso da RS" come un possibile testo JSON e prova ad analizzarlo; in caso di errore salta al prossimo RS e continua;
  3. Byte RS consecutivi non rappresentano elementi vuoti e vengono ignorati;
  4. Non c'è un marcatore di fine sequenza, quindi lo stream termina e basta;
  5. Quando il valore di livello superiore è un numero, true, false o null, il record deve terminare con un byte di spaziatura, altrimenti viene scartato come potenzialmente troncato.

La quinta regola è quella che si dimentica. Oggetti, array e stringhe in JSON sono autodelimitanti; i numeri no. <RS>123<RS> non può dirti se contiene un 123 completo o un 1234 troncato. L'RFC richiede quindi agli scrittori di aggiungere un LF, usandolo come byte sentinella per rilevare i troncamenti. Allo stesso modo, <RS>true<RS> potrebbe essere stato in origine trueish.

3. Scegliere tra JSON-seq e NDJSON

Entrambi i formati fanno streaming, ma puntano a problemi diversi:

Dimensione JSON Text Sequences NDJSON / JSON Lines
Posizione del delimitatore RS (0x1E) prima del record LF (0x0A) dopo il record
Media type application/json-seq, registrato IANA Nessuna registrazione ufficiale, convenzione della comunità
Leggibilità in byte Carattere di controllo invisibile, cat non mostra nulla Testo semplice, direttamente leggibile
Append concorrenti Il lettore può risincronizzarsi e scartare una coda troncata Anche gli append funzionano, le righe difettose vengono saltate
Numeri di livello superiore Richiedono un byte di spaziatura finale per essere affidabili Il line feed è il confine
Delimitatore dentro le stringhe RS può comparire solo come \u001e LF deve essere scritto come \n
Strumenti jq --seq e alcune librerie Quasi ogni strumento di log e big-data
Estensione del file Non definita dall'RFC .jsonl / .ndjson

La decisione è breve: usa JSON-seq quando controlli entrambe le estremità, vuoi un media type formale e ti serve che il lettore si risincronizzi dopo un troncamento; usa JSON Lines e NDJSON quando i dati alimentano sistemi di log, devono essere leggibili dalle persone o devono funzionare con gli strumenti esistenti.

4. Un parser: dividi su 0x1E, poi JSON.parse

Il beneficio più tangibile di RFC 7464 è che non serve un parser JSON in streaming. Dividi per byte, poi chiama JSON.parse su ogni segmento:

/** ASCII record separator, defined by RFC 7464 as 0x1E */
const RECORD_SEPARATOR = 0x1e;

/**
 * Parses a chunk of JSON Text Sequence bytes.
 * @param buffer Bytes that have been read so far
 * @returns Successfully parsed records and the number of failed records
 */
function parseJsonSequence(buffer: Buffer): { records: unknown[]; badCount: number } {
  const records: unknown[] = [];
  let badCount = 0;
  let recordStart = buffer.indexOf(RECORD_SEPARATOR);

  while (recordStart !== -1) {
    const nextRecordStart = buffer.indexOf(RECORD_SEPARATOR, recordStart + 1);
    const recordEnd = nextRecordStart === -1 ? buffer.length : nextRecordStart;
    const rawText = buffer.subarray(recordStart + 1, recordEnd).toString('utf8').trim();

    if (rawText !== '') {
      try {
        records.push(JSON.parse(rawText));
      } catch {
        badCount += 1;
      }
    }

    recordStart = nextRecordStart;
  }

  return { records, badCount };
}

Tre note implementative:

  • La divisione a livello di byte è sicura. I byte di continuazione UTF-8 vivono nell'intervallo 0x80–0xBF, quindi non possono mai essere uguali a 0x1E. Dividere su 0x1E non può spezzare a metà un carattere multibyte.
  • Bufferizza la coda in streaming. Mentre leggi a blocchi, il segmento finale potrebbe non aver ancora raggiunto il prossimo RS. Tienilo nel buffer finché non arriva il blocco successivo; altrimenti analizzerai due volte lo stesso mezzo record.
  • Conta i fallimenti invece di interrompere. L'RFC dice ai parser di saltare al prossimo RS e continuare, quindi badCount è una metrica di avviso, non un'eccezione. Anche un record finale troncato appartiene a quel gruppo.

L'uso di memoria dipende dal record singolo più grande più un buffer, non dalla dimensione totale del file. È la stessa idea discussa in Parsing e streaming di JSON di grandi dimensioni.

5. Caratteri di controllo dentro le stringhe

JSON richiede che ogni carattere di controllo da U+0000 a U+001F sia sottoposto a escape dentro una stringa, e RS rientra in quell'intervallo. Una stringa JSON valida può quindi contenere solo i cinque caratteri \u001e, mai un byte 0x1E grezzo. È per questo che dividere su 0x1E è sempre sicuro per i dati validi.

Il pericolo vive dal lato dello scrittore:

  1. Costruire a mano il testo JSON con '\x1e', inserendo un carattere di controllo grezzo in una stringa;
  2. Costruire JSON con template literal invece che con JSON.stringify, lasciando a capo, virgolette e caratteri di controllo non sottoposti a escape;
  3. Copiare testo che contiene RS da un terminale o da un editor, dove il carattere di controllo viene inghiottito o sostituito.

Lo schema corretto è sempre generare prima il testo, poi attaccare il delimitatore:

const recordText = JSON.stringify(record);
const line = `${String.fromCharCode(RECORD_SEPARATOR)}${recordText}\n`;

JSON.stringify emette i caratteri di controllo come \u00XX, quindi un RS grezzo non può mai comparire dentro il record. Una volta che questo passaggio è corretto, la logica di divisione deve occuparsi solo dei byte e non deve mai capire la sintassi JSON.

La maggior parte dei fallimenti di questa classe si manifesta con JSON.parse che solleva Unexpected token, soprattutto il caso confuso in cui "il primo carattere è un byte di controllo invisibile". La tabella di ricerca dei messaggi di errore JSON percorre riga per riga quella classe di errori.

6. Trasporto HTTP e media type

application/json-seq è un media type registrato senza parametri obbligatori o opzionali, e il set di caratteri è fisso su UTF-8. Su HTTP contano tre pratiche:

  • Invia Accept: application/json-seq e rispondi con Content-Type: application/json-seq;
  • Per fare streaming reale, il server deve usare il trasferimento chunked e disattivare il buffering delle risposte negli intermediari come reverse proxy, CDN e strumenti di load testing;
  • Un client che legge blocco per blocco può ricevere un blocco che termina nel mezzo di un record, quindi il parser deve bufferizzare fino al prossimo RS prima di emettere un record.

Se l'altra parte capisce solo input a righe, non imporle JSON-seq. È uno standard, ma la sua penetrazione nell'ecosistema è molto inferiore a NDJSON, quindi la compatibilità dovrebbe essere decisa durante la negoziazione del protocollo, non dopo un errore di parsing.

7. Riga di comando, log e supporto degli strumenti

jq è uno dei pochi strumenti con supporto nativo; il suo flag --seq esiste proprio per RFC 7464:

printf '\x1e{"id":1}\n\x1e{"id":2}\n' | jq --seq -c '.id'

Stampa 1 e 2 su righe separate. Per un endpoint in streaming, ricorda di disattivare il buffering di curl:

curl -sN https://api.example.com/events | jq --seq -c '.type'

jq --seq legge input RFC 7464, ignora i record difettosi e continua il parsing. È particolarmente utile quando si segue un file di log ancora in scrittura: l'ultimo record può essere scritto a metà, e l'output non mostrerà un falso oggetto in coda.

7.1 Supporto nativo per ambiente

Ambiente Supporto integrato Note
jq --seq L'opzione da riga di comando più semplice
Node.js Nessuno Buffer.indexOf più JSON.parse
Python Nessuno Dividi su b'\x1e'
Go Nessuno SplitFunc personalizzata per bufio.Scanner
Browser Nessuno fetch più ReadableStream, dividi i byte manualmente

7.2 Trappole comuni

Gli errori si concentrano dal lato della lettura:

  1. Leggere con readline, che lascia il prefisso RS invisibile sulla prima riga, quindi JSON.parse fallisce sempre;
  2. Aprire un file .json-seq in un editor in cui i caratteri di controllo sono invisibili, così sembra un unico bizzarro documento JSON;
  3. Dividere con split(/\n/), che confonde gli a capo sottoposti a escape dentro un record con l'LF tra i record;
  4. Trattare RS come un carattere ordinario nella documentazione, così scompare dopo copia e incolla.

8. Quando vale la pena usarlo

Buone occasioni:

  • Possiedi sia il produttore sia il consumatore, senza strumenti di terze parti in mezzo;
  • I dati sono uno stream di eventi aggiunto continuamente e il processo può andare in crash, quindi ti serve il recupero dopo un troncamento;
  • Il protocollo merita un media type formale invece di una convenzione come .jsonl;
  • I record individuali possono essere numeri o booleani di livello superiore, dove una regola esplicita sul troncamento conta.

Cattive occasioni:

  • I log devono finire in ELK, Loki o un altro sistema che capisce solo input a righe;
  • Le persone devono fare cat, grep e awk direttamente sui dati;
  • L'altra parte sa analizzare solo application/json.

9. Checklist

  • [ ] Lo scrittore genera i record con JSON.stringify, poi antepone 0x1E e aggiunge \n;
  • [ ] I record che contengono numeri, booleani o null di livello superiore terminano con LF;
  • [ ] Il lettore divide sui byte invece di leggere riga per riga;
  • [ ] Le letture in streaming tengono una coda non chiusa nel buffer finché non arriva il prossimo RS;
  • [ ] I fallimenti di parsing vengono contati e saltati, mai fatali per la sequenza;
  • [ ] Le interfacce pubbliche dichiarano esplicitamente application/json-seq e mantengono un fallback NDJSON.

FAQ

Devo adottare JSON-seq per un endpoint API pubblico?

Solo quando entrambe le estremità possono essere modificate o il formato viene negoziato in anticipo. JSON-seq è uno standard registrato con il media type application/json-seq, ma la sua penetrazione nell'ecosistema è molto inferiore a NDJSON, quindi imporlo a un client di terze parti di solito fallisce. Decidi la compatibilità durante la negoziazione del protocollo e mantieni un fallback NDJSON.

Devo convertire una pipeline NDJSON esistente in JSON-seq?

Di solito no. NDJSON fa già streaming, append e salta le righe difettose, e quasi ogni strumento di log e big-data lo accetta. Converti solo quando ti serve il media type registrato IANA o la risincronizzazione offerta da un delimitatore iniziale, e solo quando produttore e consumatore possono essere modificati insieme.

Quale è più comune, JSON Text Sequences o NDJSON?

NDJSON. Le sue regole sono più semplici, la copertura degli strumenti è molto più ampia, e i sistemi di log e le pipeline big-data lo accettano in genere. JSON-seq vince sul media type registrato IANA e sulla risincronizzazione che deriva dal mettere il delimitatore all'inizio, il che si adatta ai protocolli di streaming controllati più che allo scambio di dati generale.

Perché il delimitatore è posto prima del record e non dopo?

Perché i record possono essere troncati. Con un delimitatore iniziale, un lettore che vede RS sa che è iniziato un nuovo record, quindi il record precedente non può corrompere il successivo, che fosse completo o no. Con un delimitatore finale, una scrittura interrotta lascia un segmento finale che non ha né un marcatore di fine né un marcatore di inizio successivo, quindi il confine può solo essere indovinato.

Cosa succede se 0x1E compare dentro un record?

Una stringa JSON valida deve sottoporre a escape RS come \u001e, quindi un 0x1E grezzo non dovrebbe mai comparire. Se compare, il testo non è più JSON valido: dividere su 0x1E si romperà nel posto sbagliato e JSON.parse fallirà. La correzione appartiene al lato dello scrittore: usa JSON.stringify invece di testo costruito a mano.

Un browser può analizzare application/json-seq direttamente?

No. I browser non includono un parser JSON-seq; fetch consegna solo i byte. Devi usare response.body.getReader(), accumulare i blocchi fino al prossimo 0x1E e chiamare JSON.parse su ogni segmento. Per i payload piccoli, funziona anche leggere prima l'intero corpo come stringa e dividere dopo.

Perché un numero di livello superiore richiede un LF finale?

Oggetti, array e stringhe in JSON sono autodelimitanti, mentre i numeri no: <RS>123<RS> potrebbe essere un 123 completo o un 1234 troncato. RFC 7464 richiede quindi un byte di spaziatura dopo ogni record, e l'LF agisce come sentinella per rilevare i troncamenti. La stessa regola vale per true, false e null di livello superiore.