JSON Text Sequences (RFC 7464): Streaming-JSON mit RS-Trenner
Fazit: RFC 7464 verwendet einen 0x1E-Trenner, um JSON-Texte zu streamen, die sich gleichzeitig lesen und schreiben lassen. Behandelt die Regeln, den Unterschied zu NDJSON und eine Umsetzung mit Trennen und Parsen.

Kurz gesagt: Verwenden Sie JSON-seq, wenn Sie beide Enden kontrollieren, einen registrierten Medientyp brauchen und der Leser nach einer Kürzung wieder synchronisieren soll; verwenden Sie NDJSON, wenn die Daten in Log-Systeme fließen, für Menschen lesbar sein müssen oder mit bestehenden Werkzeugen funktionieren sollen – zwingen Sie JSON-seq niemals einem Gegenüber auf, das nur zeilengetrennte Eingaben versteht.
NDJSON trennt Datensätze an Zeilenumbrüchen, doch ein Zeilenumbruch ist eine menschliche Konvention. RFC 7464 schreibt die Grenze in den Byte-Stream selbst: ein unsichtbares 0x1E vor jedem Datensatz.
1. Das Problem, das ein einzelnes Byte löst
RFC 7464 wurde im Februar 2015 unter dem Titel JSON Text Sequences veröffentlicht und registriert den Medientyp application/json-seq. Er führt keine neuen Datentypen ein. Er definiert nur, wie eine Folge von JSON-Texten innerhalb eines Byte-Streams angeordnet wird. Die ganze Regel passt in einen Satz: vor jeden JSON-Text ein RS setzen und nach ihm ein LF.
- RS ist der ASCII-Record-Separator, Codepoint
0x1E, in Unicode als INFORMATION SEPARATOR TWO (U+001E) bekannt; - LF ist der Zeilenvorschub,
0x0A.
Zwei Datensätze sehen auf Byte-Ebene so aus:
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
Das RS steht vor dem Datensatz, also entgegengesetzt zum Aufbau von NDJSON, das „den Inhalt schreibt und dann den Trenner schreibt“. Diese Richtung verändert das Wiederherstellungsverhalten: Ein Leser, der 0x1E sieht, weiß, dass der vorherige Datensatz beendet ist, sodass selbst ein mitten im Schreiben abgeschnittener Datensatz die Grenze des nächsten nicht verwischen kann.
Das ist kein theoretisches Design. Der RFC erwähnt ausdrücklich an Logdateien angehängte Datensätze, die nach einem Absturz oder administrativen Kill vom Dateisystem gekürzt werden, und verlangt, dass ein Parser deshalb nicht das ganze Lesen abbricht.
2. Zwei Regelsätze: Schreiber und Parser
RFC 7464 stellt ungewöhnlicherweise zwei ABNF-Grammatiken bereit, eine für die Beschränkung der Schreiber und eine für die Lockerung der Leser.
| Rolle | Grammatik | Bedeutung |
|---|---|---|
| Schreiber | *(RS JSON-text LF) |
Ein RS vor jedem JSON-Text, ein LF nach ihm |
| Parser | *(1*RS possible-JSON) |
Aufeinanderfolgende RS-Bytes sind erlaubt, und das LF zwischen Datensätzen ist optional |
In technischen Begriffen gibt es fünf harte Regeln:
- UTF-8 ist Pflicht; UTF-16 und UTF-32 dürfen nicht verwendet werden;
- Der Parser behandelt „jedes Byte außer RS“ als möglichen JSON-Text und versucht, ihn zu parsen; bei einem Fehler springt er zum nächsten RS und macht weiter;
- Aufeinanderfolgende RS-Bytes stellen keine leeren Elemente dar und werden ignoriert;
- Es gibt keine Endmarkierung, der Stream endet einfach;
- Ist der oberste Wert eine Zahl,
true,falseodernull, muss der Datensatz mit einem Whitespace-Byte enden, sonst wird er als möglicherweise abgeschnitten verworfen.
Die fünfte Regel wird vergessen. Objekte, Arrays und Strings sind in JSON selbstbegrenzend; Zahlen sind es nicht. <RS>123<RS> kann nicht sagen, ob es ein vollständiges 123 oder ein abgeschnittenes 1234 enthält. Der RFC verlangt deshalb, dass Schreiber ein LF anhängen und es als Kanarienzeichen zur Erkennung von Kürzungen nutzen. Ebenso könnte <RS>true<RS> ursprünglich trueish gewesen sein.
3. Auswahl zwischen JSON-seq und NDJSON
Beide Formate streamen, zielen aber auf verschiedene Probleme:
| Dimension | JSON Text Sequences | NDJSON / JSON Lines |
|---|---|---|
| Position des Trenners | RS (0x1E) vor dem Datensatz |
LF (0x0A) nach dem Datensatz |
| Medientyp | application/json-seq, bei der IANA registriert |
Keine offizielle Registrierung, Community-Konvention |
| Byte-Lesbarkeit | Unsichtbares Steuerzeichen, cat zeigt nichts |
Klartext, direkt lesbar |
| Gleichzeitige Anhänge | Der Leser kann resynchronisieren und einen abgeschnittenen Schwanz verwerfen | Anhänge funktionieren auch, schlechte Zeilen werden übersprungen |
| Zahlen auf oberster Ebene | Brauchen ein abschließendes Whitespace-Byte, um vertrauenswürdig zu sein | Der Zeilenvorschub ist die Grenze |
| Trenner in Strings | RS kann nur als \u001e vorkommen |
LF muss als \n geschrieben werden |
| Werkzeuge | jq --seq und einige Bibliotheken |
Nahezu jedes Log- und Big-Data-Werkzeug |
| Dateiendung | Nicht vom RFC festgelegt | .jsonl / .ndjson |
Die Entscheidung ist kurz: Verwenden Sie JSON-seq, wenn Sie beide Enden kontrollieren, einen formalen Medientyp wollen und der Leser nach einer Kürzung wieder synchronisieren soll; verwenden Sie JSON Lines und NDJSON, wenn die Daten in Log-Systeme fließen, für Menschen lesbar sein müssen oder mit bestehenden Werkzeugen funktionieren sollen.
4. Ein Parser: an 0x1E trennen, dann JSON.parse
Der greifbarste Vorteil von RFC 7464 ist, dass Sie keinen Streaming-JSON-Parser brauchen. Teilen Sie nach Bytes und rufen Sie JSON.parse auf jedem Segment auf:
/** 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 };
}
Drei Umsetzungshinweise:
- Trennen auf Byte-Ebene ist sicher. UTF-8-Fortsetzungsbytes liegen im Bereich
0x80–0xBF, können also nie gleich0x1Esein. Das Trennen an 0x1E kann ein Mehrbytezeichen nicht halbieren. - Beim Streamen den Schwanz puffern. Beim blockweisen Lesen hat das letzte Segment das nächste RS vielleicht noch nicht erreicht. Behalten Sie es im Puffer, bis der nächste Chunk ankommt; sonst parsen Sie denselben halben Datensatz zweimal.
- Fehler zählen, statt abzubrechen. Der RFC verlangt, dass Parser zum nächsten RS springen und weitermachen,
badCountist also eine Warnmetrik, keine Ausnahme. Ein abgeschnittener letzter Datensatz gehört ebenfalls in diesen Eimer.
Der Speicherverbrauch hängt vom größten einzelnen Datensatz plus einem Puffer ab, nicht von der Gesamtdateigröße. Das ist dieselbe Idee wie in große JSON-Dateien parsen und streamen.
5. Steuerzeichen in Strings
JSON verlangt, dass jedes Steuerzeichen von U+0000 bis U+001F in einem String escaped wird, und RS fällt in diesen Bereich. Ein gültiger JSON-String kann daher nur die fünf Zeichen \u001e enthalten, nie ein rohes 0x1E-Byte. Deshalb ist das Trennen an 0x1E für gültige Daten immer sicher.
Die Gefahr liegt auf der Schreiberseite:
- JSON-Text von Hand mit
'\x1e'bauen und ein rohes Steuerzeichen in einen String setzen; - JSON mit Template-Literalen statt mit
JSON.stringifybauen und Zeilenumbrüche, Anführungszeichen und Steuerzeichen unescaped lassen; - Text mit RS aus einem Terminal oder Editor kopieren, wo das Steuerzeichen verschluckt oder ersetzt wird.
Das richtige Muster ist immer, zuerst den Text zu erzeugen und dann den Trenner anzuhängen:
const recordText = JSON.stringify(record);
const line = `${String.fromCharCode(RECORD_SEPARATOR)}${recordText}\n`;
JSON.stringify gibt Steuerzeichen als \u00XX aus, ein rohes RS kann also nie im Datensatz erscheinen. Ist dieser Schritt korrekt, muss die Trennung nur noch Bytes behandeln und nie die JSON-Syntax verstehen.
Die meisten Fehler dieser Art zeigen sich als JSON.parse-Wurf Unexpected token, besonders der verwirrende Fall „das erste Zeichen ist ein unsichtbares Steuerbyte“. Die Nachschlagetabelle für JSON-Fehlermeldungen geht diese Fehlerklasse Zeile für Zeile durch.
6. HTTP-Transport und der Medientyp
application/json-seq ist ein registrierter Medientyp ohne Pflicht- oder Optionsparameter, und der Zeichensatz ist auf UTF-8 festgelegt. Auf HTTP sind drei Praktiken wichtig:
Accept: application/json-seqsenden und mitContent-Type: application/json-seqantworten;- Für echtes Streaming muss der Server Chunked Transfer verwenden und die Pufferung der Antwort in Zwischeninstanzen wie Reverse Proxies, CDNs und Lasttestwerkzeugen deaktivieren;
- Ein blockweise lesender Client kann einen Chunk erhalten, der mitten in einem Datensatz endet, der Parser muss also bis zum nächsten RS puffern, bevor er einen Datensatz ausgibt.
Versteht die andere Seite nur zeilengetrennte Eingaben, zwingen Sie ihr kein JSON-seq auf. Es ist ein Standard, aber seine Verbreitung im Ökosystem liegt weit unter NDJSON, Kompatibilität sollte also in der Protokollverhandlung entschieden werden, nicht nach einem Parse-Fehler.
7. Kommandozeile, Logs und Werkzeugunterstützung
jq ist eines der wenigen Werkzeuge mit nativer Unterstützung; sein --seq-Flag existiert genau für RFC 7464:
printf '\x1e{"id":1}\n\x1e{"id":2}\n' | jq --seq -c '.id'
Das gibt 1 und 2 in getrennten Zeilen aus. Bei einem Streaming-Endpunkt denken Sie daran, die Pufferung von curl zu deaktivieren:
curl -sN https://api.example.com/events | jq --seq -c '.type'
jq --seq liest RFC-7464-Eingaben, ignoriert schlechte Datensätze und parst weiter. Das ist besonders nützlich, wenn Sie eine noch wachsende Logdatei verfolgen: Der letzte Datensatz kann halb geschrieben sein, und die Ausgabe zeigt kein falsches abschließendes Objekt.
7.1 Native Unterstützung nach Umgebung
| Umgebung | Eingebaute Unterstützung | Hinweise |
|---|---|---|
| jq | --seq |
Die einfachste Kommandozeilenoption |
| Node.js | Keine | Buffer.indexOf plus JSON.parse |
| Python | Keine | An b'\x1e' trennen |
| Go | Keine | Eigener SplitFunc für bufio.Scanner |
| Browser | Keine | fetch plus ReadableStream, Bytes manuell trennen |
7.2 Häufige Fallstricke
Die Fehler häufen sich auf der Leseseite:
- Mit
readlinelesen, was das unsichtbare RS-Präfix an der ersten Zeile lässt, sodassJSON.parseimmer scheitert; - Eine
.json-seq-Datei in einem Editor öffnen, in dem Steuerzeichen unsichtbar sind, sodass sie wie ein seltsames JSON-Dokument aussieht; - Mit
split(/\n/)trennen, was escaped Zeilenumbrüche innerhalb eines Datensatzes mit dem LF zwischen Datensätzen verwechselt; - RS in der Dokumentation als gewöhnliches Zeichen behandeln, sodass es nach Kopieren und Einfügen verschwindet.
8. Wann es sich lohnt
Gut geeignet:
- Sie besitzen sowohl den Erzeuger als auch den Konsumenten, ohne Drittanbieterwerkzeuge dazwischen;
- Die Daten sind ein kontinuierlich angehängter Ereignisstrom und der Prozess kann abstürzen, Sie brauchen also Wiederherstellung nach Kürzungen;
- Das Protokoll verdient einen formalen Medientyp statt einer Konvention wie
.jsonl; - Einzelne Datensätze können Zahlen oder Booleans auf oberster Ebene sein, wo eine ausdrückliche Kürzungsregel zählt.
Schlecht geeignet:
- Logs müssen in ELK, Loki oder ein anderes System fließen, das nur zeilengetrennte Eingaben versteht;
- Menschen müssen die Daten direkt mit
cat,grepundawkbearbeiten; - Die andere Seite kann nur
application/jsonparsen.
9. Checkliste
- [ ] Der Schreiber erzeugt Datensätze mit
JSON.stringify, stellt dann0x1Evoran und hängt\nan; - [ ] Datensätze mit Zahlen, Booleans oder
nullauf oberster Ebene enden mit LF; - [ ] Der Leser trennt nach Bytes, statt zeilenweise zu lesen;
- [ ] Streaming-Lesen behält einen nicht geschlossenen Schwanz im Puffer, bis das nächste RS kommt;
- [ ] Parse-Fehler werden gezählt und übersprungen, nie fatal für die Sequenz;
- [ ] Öffentliche Schnittstellen deklarieren
application/json-seqausdrücklich und halten einen NDJSON-Fallback bereit.
FAQ
Sollte ich JSON-seq für einen öffentlichen API-Endpunkt übernehmen?
Nur wenn beide Enden geändert werden können oder das Format im Voraus verhandelt wird. JSON-seq ist ein registrierter Standard mit dem Medientyp application/json-seq, aber seine Verbreitung im Ökosystem liegt weit unter NDJSON, einem Drittanbieter-Client aufzuzwingen scheitert daher meist. Entscheiden Sie die Kompatibilität in der Protokollverhandlung und halten Sie einen NDJSON-Fallback bereit.
Sollte ich eine bestehende NDJSON-Pipeline auf JSON-seq umstellen?
Meist nicht. NDJSON streamt bereits, hängt an und überspringt schlechte Zeilen, und nahezu jedes Log- und Big-Data-Werkzeug akzeptiert es. Stellen Sie nur um, wenn Sie den IANA-registrierten Medientyp oder die Resynchronisation eines vorangestellten Trenners brauchen – und nur, wenn Erzeuger und Konsument gemeinsam geändert werden können.
Was ist verbreiteter, JSON Text Sequences oder NDJSON?
NDJSON. Seine Regeln sind einfacher, seine Werkzeugabdeckung weit breiter, und Log-Systeme sowie Big-Data-Pipelines akzeptieren es im Allgemeinen. JSON-seq gewinnt beim IANA-registrierten Medientyp und bei der Resynchronisation, die daraus folgt, dass der Trenner vorn steht – das passt eher zu kontrollierten Streaming-Protokollen als zum allgemeinen Datenaustausch.
Warum steht der Trenner vor dem Datensatz und nicht dahinter?
Weil Datensätze abgeschnitten werden können. Mit einem führenden Trenner weiß ein Leser, der RS sieht, dass ein neuer Datensatz begonnen hat, der vorherige kann den nächsten also nicht beschädigen, ob vollständig oder nicht. Mit einem abschließenden Trenner hinterlässt ein unterbrochenes Schreiben ein letztes Segment ohne Endmarkierung und ohne folgende Startmarkierung, die Grenze lässt sich also nur raten.
Was passiert, wenn 0x1E innerhalb eines Datensatzes erscheint?
Ein gültiger JSON-String muss RS als \u001e escapen, ein rohes 0x1E sollte also nie erscheinen. Tut es das, ist der Text kein gültiges JSON mehr: Das Trennen an 0x1E bricht an der falschen Stelle, und JSON.parse scheitert. Die Behebung gehört auf die Schreiberseite: JSON.stringify statt handgebauten Text verwenden.
Kann ein Browser application/json-seq direkt parsen?
Nein. Browser bringen keinen JSON-seq-Parser mit; fetch liefert nur die Bytes. Sie müssen response.body.getReader() verwenden, Chunks bis zum nächsten 0x1E ansammeln und JSON.parse auf jedem Segment aufrufen. Bei kleinen Payloads funktioniert auch, zuerst den ganzen Body als String zu lesen und danach zu trennen.
Warum braucht eine Zahl auf oberster Ebene ein abschließendes LF?
Objekte, Arrays und Strings sind in JSON selbstbegrenzend, Zahlen nicht: <RS>123<RS> könnte ein vollständiges 123 oder ein abgeschnittenes 1234 sein. RFC 7464 verlangt deshalb ein Whitespace-Byte nach jedem Datensatz, und das LF wirkt als Kanarienzeichen zur Kürzungserkennung. Dieselbe Regel gilt für true, false und null auf oberster Ebene.