Posloupnosti textů JSON (RFC 7464): streamovaný JSON oddělený RS
Shrnutí: RFC 7464 používá oddělovač 0x1E pro streamování textů JSON, které lze zároveň číst i zapisovat. Pokrývá pravidla, rozdíl od NDJSON a implementaci rozdělení a parsování.

Stručně: JSON-seq použijte, když ovládáte oba konce, potřebujete registrovaný mediální typ a chcete, aby se čtečka po zkrácení znovu synchronizovala; NDJSON použijte, když data živí logovací systémy, musí být čitelná pro lidi, nebo musí fungovat se stávajícími nástroji – nikdy nevynucujte JSON-seq na protějšek, který rozumí jen řádkově oddělenému vstupu.
NDJSON dělí záznamy podle nových řádků, ale nový řádek je lidská konvence. RFC 7464 zapisuje hranici přímo do bajtového proudu: neviditelné 0x1E před každým záznamem.
1. Problém vyřešený jediným bajtem
RFC 7464 byl publikován v únoru 2015 pod názvem JSON Text Sequences a registruje mediální typ application/json-seq. Nepřidává žádné nové datové typy. Definuje jen, jak je posloupnost textů JSON uspořádána v jednom bajtovém proudu. Celé pravidlo se vejde do jedné věty: před každý text JSON dejte jeden RS a za něj jeden LF.
- RS je ASCII oddělovač záznamů, kódový bod
0x1E, v Unicode známý jako INFORMATION SEPARATOR TWO (U+001E); - LF je posun řádku,
0x0A.
Dva záznamy vypadají na úrovni bajtů takto:
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
RS sedí před záznamem, což je opak uspořádání NDJSON „napiš obsah, poté napiš oddělovač“. Tento směr mění chování při obnově: čtečka, která vidí 0x1E, ví, že předchozí záznam skončil, takže ani záznam zkrácený uprostřed zápisu nemůže rozmazat hranici následujícího.
Nejde o teoretický návrh. RFC výslovně zmiňuje záznamy připojené do logovacích souborů, které souborový systém zkrátí po pádu nebo administrativním ukončení, a říká, že parser by kvůli tomu neměl přerušit celé čtení.
2. Dvě sady pravidel: kodéry a parsery
RFC 7464 nezvykle poskytuje dvě ABNF gramatiky, jednu omezující zapisovače a jednu uvolňující čtečky.
| Role | Gramatika | Význam |
|---|---|---|
| Kodér | *(RS JSON-text LF) |
Jeden RS před každým textem JSON, jeden LF za ním |
| Parser | *(1*RS possible-JSON) |
Po sobě jdoucí bajty RS jsou povoleny a LF mezi záznamy je nepovinný |
V inženýrských termínech existuje pět tvrdých pravidel:
- UTF-8 je povinné; UTF-16 a UTF-32 se nesmějí používat;
- Parser považuje „jakýkoli bajt jiný než RS“ za možný text JSON a pokusí se ho parsovat; při selhání přeskočí k dalšímu RS a pokračuje;
- Po sobě jdoucí bajty RS nereprezentují prázdné prvky a ignorují se;
- Neexistuje značka konce posloupnosti, proud prostě skončí;
- Když je hodnota na nejvyšší úrovni číslo,
true,falsenebonull, záznam musí končit prázdným znakem, jinak je zahozen jako možná zkrácený.
Páté pravidlo je to, na které lidé zapomínají. Objekty, pole a řetězce jsou v JSON sebe vymezující; čísla ne. <RS>123<RS> vám neřekne, zda nese kompletní 123, nebo zkrácené 1234. RFC proto vyžaduje, aby zapisovači přidali LF a použili ho jako kontrolní bajt pro detekci zkrácení. Podobně <RS>true<RS> mohlo původně být trueish.
3. Volba mezi JSON-seq a NDJSON
Oba formáty streamují, ale cíli na různé problémy:
| Dimenze | JSON Text Sequences | NDJSON / JSON Lines |
|---|---|---|
| Pozice oddělovače | RS (0x1E) před záznamem |
LF (0x0A) za záznamem |
| Mediální typ | application/json-seq, registrován v IANA |
Žádná oficiální registrace, komunitní konvence |
| Čitelnost bajtů | Neviditelný řídicí znak, cat nic neukáže |
Prostý text, přímo čitelný |
| Souběžné připojování | Čtečka se umí znovu synchronizovat a zkrácený konec zahodit | Připojování funguje také, špatné řádky se přeskakují |
| Čísla na nejvyšší úrovni | Vyžaduje koncový prázdný bajt, aby bylo důvěryhodné | Hranicí je posun řádku |
| Oddělovač uvnitř řetězců | RS se může objevit jen jako \u001e |
LF se musí zapsat jako \n |
| Nástroje | jq --seq a několik knihoven |
Téměř každý logovací a big-data nástroj |
| Přípona souboru | RFC nedefinuje | .jsonl / .ndjson |
Rozhodnutí je krátké: JSON-seq použijte, když ovládáte oba konce, chcete formální mediální typ a potřebujete, aby se čtečka znovu synchronizovala po zkrácení; JSON Lines a NDJSON použijte, když data živí logovací systémy, musí být čitelná pro lidi, nebo musí fungovat se stávajícími nástroji.
4. Parser: rozdělení podle 0x1E, poté JSON.parse
Nejhmatalnější výhodou RFC 7464 je, že nepotřebujete streamovaný JSON parser. Rozdělejte podle bajtů a zavolejte JSON.parse na každý segment:
/** ASCII oddělovač záznamů, definovaný RFC 7464 jako 0x1E */
const RECORD_SEPARATOR = 0x1e;
/**
* Parsuje blok bajtů posloupnosti textů JSON.
* @param buffer Bajty dosud načtené
* @returns Úspěšně parsované záznamy a počet selhavších záznamů
*/
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 };
}
Tři poznámky k implementaci:
- Dělení na úrovni bajtů je bezpečné. Pokračovací bajty UTF-8 žijí v rozsahu
0x80–0xBF, takže nikdy nemohou být rovny0x1E. Rozdělení podle 0x1E nemůže rozpůlit vícebajtový znak. - Při streamování bufferujte ocas. Při čtení po blocích poslední segment možná ještě nedosáhl dalšího RS. Držte ho v bufferu, dokud nedorazí další blok; jinak stejný polo-záznam parsujete dvakrát.
- Počítejte selhání, místo abyste přerušovali. RFC říká parserům, aby přeskočily k dalšímu RS a pokračovaly, takže
badCountje metrika varování, nikoli výjimka. Zkrácený poslední záznam patří do stejného koše.
Spotřeba paměti závisí na největším jednotlivém záznamu plus jednom bufferu, nikoli na celkové velikosti souboru. Je to stejná myšlenka, o které se pojednává v Parsování a streamování velkého JSON.
5. Řídicí znaky uvnitř řetězců
JSON vyžaduje, aby každý řídicí znak od U+0000 do U+001F byl uvnitř řetězce escapován, a RS spadá do tohoto rozsahu. Platný řetězec JSON proto může obsahovat jen pět znaků \u001e, nikdy surový bajt 0x1E. Proto je dělení podle 0x1E u platných dat vždy bezpečné.
Nebezpečí žije na straně zapisovače:
- Ruční stavění textu JSON pomocí
'\x1e', vkládání surového řídicího znaku do řetězce; - Stavění JSON pomocí šablonových literálů místo
JSON.stringify, s neescapovanými novými řádky, uvozovkami a řídicími znaky; - Kopírování textu obsahujícího RS z terminálu nebo editoru, kde se řídicí znak spolkne nebo nahradí.
Správný vzor je vždy nejprve vygenerovat text, poté připojit oddělovač:
const recordText = JSON.stringify(record);
const line = `${String.fromCharCode(RECORD_SEPARATOR)}${recordText}\n`;
JSON.stringify vypisuje řídicí znaky jako \u00XX, takže surový RS se uvnitř záznamu nikdy nemůže objevit. Jakmile je tento krok správný, logika dělení se musí zabývat jen bajty a nikdy nemusí rozumět syntaxi JSON.
Většina selhání v této kategorii se projeví jako JSON.parse vyhazující Unexpected token, zejména matoucí případ, kdy „první znak je neviditelný řídicí bajt“. Vyhledávací tabulka chybových zpráv JSON prochází tuto třídu chyb řádek po řádku.
6. HTTP přenos a mediální typ
application/json-seq je registrovaný mediální typ bez povinných nebo volitelných parametrů a znaková sada je pevně UTF-8. Na HTTP záleží na třech praktikách:
- Posílejte
Accept: application/json-seqa odpovídejteContent-Type: application/json-seq; - Pro skutečné streamování musí server použít chunked transfer a vypnout bufferování odpovědi v prostřednících, jako jsou reverzní proxy, CDN a nástroje pro zátěžové testy;
- Klient čtoucí blok po bloku může dostat blok, který končí uprostřed záznamu, takže parser musí bufferovat až do dalšího RS, než záznam vydá.
Pokud protější strana rozumí jen řádkově oddělenému vstupu, nevynucujte na ni JSON-seq. Je to standard, ale jeho proniknutí do ekosystému je hluboko pod NDJSON, takže kompatibilita se má rozhodnout během vyjednávání protokolu, nikoli po selhání parsování.
7. Příkazový řádek, logy a podpora nástrojů
jq je jedním z mála nástrojů s nativní podporou; jeho přepínač --seq existuje právě pro RFC 7464:
printf '\x1e{"id":1}\n\x1e{"id":2}\n' | jq --seq -c '.id'
To vypíše 1 a 2 na samostatných řádcích. Pro streamovací endpoint pamatujte na vypnutí bufferování curl:
curl -sN https://api.example.com/events | jq --seq -c '.type'
jq --seq čte vstup RFC 7464, ignoruje špatné záznamy a pokračuje v parsování. To je zvlášť užitečné při sledování logovacího souboru, do kterého se stále zapisuje: poslední záznam může být napůl zapsaný a výstup neukáže falešný koncový objekt.
7.1 Nativní podpora podle prostředí
| Prostředí | Vestavěná podpora | Poznámky |
|---|---|---|
| jq | --seq |
Nejsnadnější možnost z příkazového řádku |
| Node.js | Žádná | Buffer.indexOf plus JSON.parse |
| Python | Žádná | Rozdělení podle b'\x1e' |
| Go | Žádná | Vlastní SplitFunc pro bufio.Scanner |
| Prohlížeč | Žádná | fetch plus ReadableStream, ruční dělení bajtů |
7.2 Časté pasti
Chyby se shlukují na straně čtení:
- Čtení pomocí
readline, které nechává neviditelný prefix RS na prvním řádku, takžeJSON.parsevždy selže; - Otevření souboru
.json-seqv editoru, kde jsou řídicí znaky neviditelné, takže vypadá jako jeden bizarní dokument JSON; - Dělení pomocí
split(/\n/), které slévá escapované nové řádky uvnitř záznamu s LF mezi záznamy; - Považování RS za obyčejný znak v dokumentaci, takže po kopírování a vložení zmizí.
8. Kdy se vyplatí
Dobré shody:
- Vlastníte producenta i konzumenta a mezi nimi nejsou nástroje třetích stran;
- Data jsou průběžně připojovaný proud událostí a proces může spadnout, takže potřebujete obnovu ze zkrácení;
- Protokol si zaslouží formální mediální typ, nikoli konvenci jako
.jsonl; - Jednotlivé záznamy mohou být čísla nebo booleany na nejvyšší úrovni, kde záleží na explicitním pravidle zkrácení.
Špatné shody:
- Logy musí jít do ELK, Loki nebo jiného systému, který rozumí jen řádkově oddělenému vstupu;
- Lidé potřebují data přímo
cat,grepaawk; - Protější strana umí parsovat jen
application/json.
9. Kontrolní seznam
- [ ] Zapisovač generuje záznamy pomocí
JSON.stringify, poté předpojí0x1Ea připojí\n; - [ ] Záznamy s čísly, booleany nebo
nullna nejvyšší úrovni končí LF; - [ ] Čtečka dělí podle bajtů, místo aby četla řádek po řádku;
- [ ] Streamované čtení drží neuzavřený ocas v bufferu, dokud nedorazí další RS;
- [ ] Selhání parsování se počítají a přeskakují, nikdy nejsou pro posloupnost fatální;
- [ ] Veřejná rozhraní explicitně deklarují
application/json-seqa udržují záložní NDJSON.
Časté dotazy
Mám přijmout JSON-seq pro endpoint veřejného API?
Jen když lze změnit oba konce, nebo když se formát vyjedná předem. JSON-seq je registrovaný standard s mediálním typem application/json-seq, ale jeho proniknutí do ekosystému je hluboko pod NDJSON, takže vynucení na klienta třetí strany obvykle selhává. Rozhodněte kompatibilitu během vyjednávání protokolu a udržujte záložní NDJSON.
Mám převést stávající pipeline NDJSON na JSON-seq?
Obvykle ne. NDJSON už streamuje, připojuje a přeskakuje špatné řádky a přijímá ho téměř každý logovací a big-data nástroj. Převádějte jen tehdy, když potřebujete mediální typ registrovaný v IANA nebo resynchronizaci, kterou poskytuje úvodní oddělovač, a jen když lze producenta i konzumenta změnit společně.
Co je běžnější, JSON Text Sequences, nebo NDJSON?
NDJSON. Jeho pravidla jsou jednodušší, pokrytí nástroji mnohem širší a logovací systémy i big-data pipeline ho obecně přijímají. JSON-seq vítězí mediálním typem registrovaným v IANA a resynchronizací, která plyne z umístění oddělovače dopředu, což se hodí pro řízené streamovací protokoly, nikoli pro obecnou výměnu dat.
Proč je oddělovač umístěn před záznamem, nikoli za ním?
Protože záznamy mohou být zkráceny. S úvodním oddělovačem čtečka, která vidí RS, ví, že začal nový záznam, takže předchozí záznam nemůže poškodit následující, ať byl kompletní, nebo ne. S koncovým oddělovačem přerušený zápis nechává poslední segment bez koncové i následné startovní značky, takže hranici lze jen hádat.
Co se stane, když se 0x1E objeví uvnitř záznamu?
Platný řetězec JSON musí escapovat RS jako \u001e, takže surový 0x1E by se nikdy neměl objevit. Pokud se objeví, text už není platný JSON: dělení podle 0x1E se zlomí na špatném místě a JSON.parse selže. Oprava patří na stranu zapisovače: použijte JSON.stringify místo ručně sestaveného textu.
Umí prohlížeč parsovat application/json-seq přímo?
Ne. Prohlížeče neobsahují parser JSON-seq; fetch dodá jen bajty. Musíte použít response.body.getReader(), shromažďovat bloky až do dalšího 0x1E a volat JSON.parse na každém segmentu. Pro malé payloady funguje i nejprve načíst celé tělo jako řetězec a poté rozdělit.
Proč číslo na nejvyšší úrovni potřebuje koncový LF?
Objekty, pole a řetězce jsou v JSON sebe vymezující, zatímco čísla ne: <RS>123<RS> mohlo být kompletní 123 nebo zkrácené 1234. RFC 7464 proto vyžaduje prázdný bajt za každým záznamem a LF slouží jako kontrolní bajt pro detekci zkrácení. Stejné pravidlo platí pro true, false a null na nejvyšší úrovni.