JSON Toolkit

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.

Fortgeschritten~10 Min. LesezeitVeröffentlicht 2026-10-30Aktualisiert 2026-10-30
Titelbild für JSON Lines und NDJSON Streaming-Daten

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:

  1. Kodierung ist UTF-8, ohne BOM;
  2. Jede Zeile enthält einen vollständigen, gültigen JSON-Wert;
  3. Zeilen werden durch \n getrennt; die letzte Zeile kann mit oder ohne Zeilenumbruch enden;
  4. Innerhalb einer Zeile darf kein unescapter Zeilenumbruch vorkommen – ein Zeilenumbruch in einem String muss als \n geschrieben werden;
  5. 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:

  1. Die Ausgabe von JSON.stringify enthält keine Zeilenumbrüche (außer das \n eines Strings wird als zwei Zeichen escaped), eine Zeile ist also immer genau ein Datensatz;
  2. Wenn mehrere Prozesse gleichzeitig anhängen, ist ein einzelner write kleiner 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 .jsonl mit konstant hohem Verhältnis (Feldnamen wiederholen sich), .jsonl.gz ist 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 1GB aufteilen, mit Sequenznummern in den Namen, um parallele Verarbeitung und wiederaufnehmbare Übertragung zu erleichtern;
  • .jsonl niemals 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 (.jsonl oder .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.