JSON Lines und NDJSON: Streaming-Daten
Fazit: Ein JSON-Wert pro Zeile erkauft Anfügbarkeit, Streaming und auf eine Zeile begrenzte Schäden; der Preis ist das Fehlen einer Gesamtstruktur, weshalb zeilenweise validiert werden muss.

Kurz gesagt: Verwenden Sie ein JSON-Array, wenn Sie einen ganzen Stapel auf einmal verarbeiten, und JSON Lines, wenn Sie Streaming-Anhänge und Isolation schlechter Zeilen brauchen; müssen Sie dennoch die Gesamtstruktur validieren, wechseln Sie nicht zu einem zeilengetrennten Format.
Sobald die Daten den Speicher übersteigen oder kontinuierlich eintreffen, wird die Struktur „ein großes Array“ zum Feind.
1. Formatregeln
JSON Lines (.jsonl) und NDJSON (.ndjson) sind praktisch Synonyme, und die Regeln sind in beiden Fällen kurz:
- Kodierung ist UTF-8, ohne BOM;
- Jede Zeile enthält einen vollständigen, gültigen JSON-Wert;
- Zeilen werden durch
\ngetrennt; die letzte Zeile kann mit oder ohne Zeilenumbruch enden; - Innerhalb einer Zeile darf kein unescapter Zeilenumbruch vorkommen – ein Zeilenumbruch in einem String muss als
\ngeschrieben werden; - Leere Zeilen werden meist ignoriert, eine strenge Implementierung sollte sie aber als ungültig behandeln.
{"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"}
Der feine Unterschied zwischen beiden: Die JSON-Lines-Website betont „ein Wert pro Zeile, und der Wert darf jedes JSON sein (auch Skalare)“, während die NDJSON-Spezifikation strengere Umsetzungsregeln ergänzt wie „keine leeren Zeilen und keine Skalare auf oberster Ebene“. In der Technik bleibt man bei der strengeren Variante aus dem Ärger heraus.
2. Vergleich mit einem JSON-Array
| Dimension | JSON-Array | JSON Lines |
|---|---|---|
| Einen Datensatz anhängen | Die ganze Datei neu schreiben (schließendes ] ändern) |
Nur eine Zeile anhängen |
| Streaming-Lesen | Braucht einen Streaming-Parser, der die Klammertiefe verfolgt | Zeilenweise trennen |
| Ein beschädigter Eintrag | Die ganze Datei wird unparsbar | Nur diese Zeile ist betroffen |
| Validierung der ganzen Datei | Alles in einem Durchlauf validieren | Nur zeilenweise |
| Kompression | Ähnliche Größe nach gzip | Gleich, und Blöcke lassen sich parallel komprimieren |
| Verschachtelung ausdrücken | Nativ unterstützt | Jede Zeile kann intern verschachteln; Zeilen sind unabhängig voneinander |
| Typische Konsumenten | Alle JSON-Werkzeuge | Log-Systeme, Big-Data-Pipelines, LLM-Trainingsdaten |
Die Schlussfolgerung ist direkt: ob die Daten „ein Stapel Datensätze“ oder „ein Dokument“ sind, entscheidet über das Format.
3. Lesen: zeilenweises Streaming
In Node ist der einfachste Weg 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 };
}
Der Speicherverbrauch dieses Codes hängt nur von der Größe einer einzelnen Zeile ab, nicht von der Gesamtdateigröße – das ist der größte Wert eines zeilengetrennten Formats.
3.1 Brauchen Sie nur wenige Felder, nicht zuerst JSON.parse
Hat jeder Datensatz 50 Felder und Sie brauchen nur 3, baut JSON.parse Objekte für alle 50.
Ein Streaming-Parser (stream-json, clarinet) kann nur die Ziel-Felder extrahieren, zum Preis höherer Umsetzungskomplexität.
Die Faustregel: Ist ein einzelner Datensatz unter einigen hundert KB groß, ist der direkte Aufruf von JSON.parse meist immer noch das bessere Geschäft.
3.2 Chinesischer Text und BOMs
Unter Windows erzeugte .jsonl-Dateien tragen oft ein BOM, und JSON.parse in der ersten Zeile scheitert garantiert. Entfernen Sie das BOM vor dem Lesen, oder prüfen Sie beim Streamen des ersten Chunks die ersten drei Bytes.
4. Schreiben: Anhänge und Atomarität
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' });
}
Zwei Dinge sind zu beachten:
- Die Ausgabe von
JSON.stringifyenthält keine Zeilenumbrüche (außer das\neines Strings wird als zwei Zeichen escaped), eine Zeile ist also immer genau ein Datensatz; - Wenn mehrere Prozesse gleichzeitig anhängen, ist ein einzelner
writekleiner als PIPE_BUF (unter Linux üblicherweise 4096 Bytes) atomar; alles Größere ist nicht garantiert. Für Szenarien mit hoher Nebenläufigkeit wechseln Sie zu „eine Datei pro Prozess, nach dem Flush auf die Platte zusammengeführt“ oder verwenden Sie Speicher mit Transaktionen.
5. Umgang mit schlechten Zeilen
Das ist die Entscheidung, die ein zeilengetrenntes Format im Voraus erzwingt:
| Strategie | Geeignet für | Kosten |
|---|---|---|
| Überspringen und zählen | Daten, die Lücken vertragen, etwa Logs und Analyseereignisse | Daten gehen stillschweigend verloren, Alarmierung ist Pflicht |
| Sofort scheitern | Daten, die nicht verloren gehen dürfen, etwa Abrechnung und Rechnungen | Eine schlechte Zeile blockiert den ganzen Stapel |
| In eine Dead-Letter-Datei leiten | Fälle, die später nachgefüllt werden müssen | Zusätzlicher Speicher und ein Replay-Prozess |
Wie auch immer Sie wählen, protokollieren Sie die Zeilennummer und ein Fragment des Originalinhalts, sonst können Sie das Problem später nicht lokalisieren. Die Zeilennummer steht nicht in der Fehlermeldung von JSON.parse, Sie müssen sie selbst pflegen.
6. Kompression und Sharding
- gzip komprimiert
.jsonlmit konstant hohem Verhältnis (Feldnamen wiederholen sich),.jsonl.gzist daher empfehlenswert; - gzip komprimiert blockweise und unterstützt nativ Streaming-Dekompression, die ganze Datei muss also nicht entpackt werden;
- Große Dateien in Scheiben von etwa
1GBaufteilen, mit Sequenznummern in den Namen, um parallele Verarbeitung und wiederaufnehmbare Übertragung zu erleichtern; .jsonlniemals durch „in ein Array zusammenführen“ vorverarbeiten – das wirft alle seine Vorteile weg.
7. Abwägung gegenüber anderen Big-Data-Formaten
| Format | Struktur | Schema enthalten | Gut für |
|---|---|---|---|
| JSON Lines | Unabhängig pro Zeile | Nein (braucht einen externen Vertrag) | Logs, Importe und Exporte, Trainingsdaten |
| CSV | Tabellarisch | Nein | Berichte, Excel-Austausch |
| Parquet | Spaltenorientiert | Ja | Analytische Abfragen, sehr große Offline-Datensätze |
| Avro | Zeilenorientiert | Ja | Kafka-Nachrichten, Schema-Evolution |
Der Vorteil von JSON Lines ist, dass jede Sprache und jedes Werkzeug es sofort lesen und schreiben kann; der Nachteil ist das Fehlen eines Schemas und spaltenorientierter Kompression. Sobald die Daten nicht mehr auf eine Maschine passen und Sie häufige spaltenorientierte Abfragen brauchen, ist es Zeit, Parquet in Betracht zu ziehen.
8. Checkliste
- [ ] Die Dateiendung ist einheitlich (
.jsonloder.ndjson) und wird nie gemischt; - [ ] Der Schreiber garantiert „ein Datensatz pro Zeile“, mit bereits escaped Zeilenumbrüchen in Strings;
- [ ] Der Leser entfernt das BOM, überspringt leere Zeilen und pflegt Zeilennummern;
- [ ] Die Strategie für schlechte Zeilen ist ausdrücklich definiert und überwacht (schlechte Zeilen / Gesamtzeilen);
- [ ] Große Dateien werden nach Größe in Scheiben geteilt und mit gzip und parallelem Lesen kombiniert;
- [ ] Wo ein Feldvertrag besteht, wird ein zusätzliches Schema bereitgestellt und zeilenweise validiert.
FAQ
Wie wähle ich zwischen einem JSON-Array und JSON Lines?
Sehen Sie darauf, ob die Daten ein Stapel Datensätze oder ein einzelnes Dokument sind. Wählen Sie JSON Lines, wenn Datensätze angehängt und als Stream gelesen werden: Anhängen heißt, eine Zeile ans Ende zu schreiben, und Lesen erfordert nur das Trennen nach Zeilen. Behalten Sie ein JSON-Array, wenn die ganze Struktur in einem Durchlauf validiert werden muss, denn ein JSON-Array validiert alles auf einmal, während JSON Lines nur zeilenweise validiert werden kann.
Warum bricht eine beschädigte Zeile nicht die ganze Datei?
Jede Zeile in JSON Lines ist ein unabhängiger, vollständiger JSON-Wert, und das Parsen erfolgt naturgemäß zeilenweise, beschädigte Daten betreffen also nur diese Zeile. In einem JSON-Array macht ein Syntaxfehler die ganze Datei unparsbar – genau deshalb passen zeilengetrennte Daten zu Logs und Analyseereignissen.
Sollte ich JSON Lines in ein JSON-Array umwandeln?
Das können Sie: zeilenweise lesen, jeden Datensatz mit JSON.parse parsen und in einem Array sammeln. Dafür muss der gesamte Inhalt in den Speicher passen, und Streaming-Lesen sowie Ein-Zeilen-Anhänge gehen verloren. .jsonl zu einem Array vorzuverarbeiten wirft alle Vorteile des Formats weg, ist also nur für eine einmalige Auslieferung sinnvoll, bei der die Daten in den Speicher passen.