JSON Lines i NDJSON: dane strumieniowe
Podsumowanie: Jedna wartość JSON w wierszu kupuje możliwość dopisywania, strumieniowanie i izolację uszkodzenia do jednej linii; kosztem jest brak struktury całościowej, więc walidację trzeba wykonywać linia po linii.

Najważniejsze: używaj tablicy JSON, gdy przetwarzasz całą partię naraz, a JSON Lines, gdy potrzebujesz strumieniowego dopisywania i izolacji złych linii; jeśli nadal musisz walidować strukturę całościową, nie przechodź na format liniowy.
Gdy dane przekraczają pamięć albo napływają ciągle, struktura „jednej wielkiej tablicy” staje się wrogiem.
1. Reguły formatu
JSON Lines (.jsonl) i NDJSON (.ndjson) to praktycznie synonimy, a reguły w obu przypadkach są krótkie:
- Kodowanie to UTF-8, bez BOM;
- Każdy wiersz zawiera jedną kompletną, poprawną wartość JSON;
- Wiersze są rozdzielane
\n; ostatni wiersz może, ale nie musi kończyć się nową linią; - W wierszu nie może wystąpić nieescape'owany znak nowej linii — nową linię wewnątrz łańcucha trzeba zapisać jako
\n; - Puste wiersze są zwykle ignorowane, ale ścisła implementacja powinna traktować je jako niepoprawne.
{"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"}
Subtelna różnica między nimi: strona JSON Lines podkreśla „jedna wartość na wiersz, a wartość może być dowolnym JSON-em (w tym skalarem)”, podczas gdy specyfikacja NDJSON dodaje ściślejsze reguły implementacyjne, takie jak „brak pustych wierszy i brak skalarnych wierzchołków”. W inżynierii trzymanie się bardziej rygorystycznego zestawu chroni przed kłopotami.
2. Porównanie z tablicą JSON
| Wymiar | Tablica JSON | JSON Lines |
|---|---|---|
| Dopisanie jednego rekordu | Przepisanie całego pliku (zmiana zamykającego ]) |
Dopisanie jednej linii |
| Odczyt strumieniowy | Wymaga parsera strumieniowego śledzącego głębokość nawiasów | Podział po wierszach |
| Jedna uszkodzona pozycja | Cały plik staje się nieparsowalny | Dotknięta tylko ta linia |
| Walidacja całego pliku | Walidacja wszystkiego jednym przebiegiem | Tylko linia po linii |
| Kompresja | Podobny rozmiar po gzip | To samo, a bloki można kompresować równolegle |
| Wyrażanie zagnieżdżenia | Natywne wsparcie | Każda linia może zagnieżdżać wewnętrznie; linie są niezależne |
| Typowi konsumenci | Wszystkie narzędzia JSON | Systemy logów, potoki big data, zestawy treningowe LLM |
Wniosek jest bezpośredni: to, czy dane są „partię rekordów”, czy „jednym dokumentem”, decyduje o wyborze formatu.
3. Odczyt: strumieniowanie linia po linii
W Node najprostszym podejściem jest 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 };
}
Zużycie pamięci tego kodu zależy tylko od rozmiaru pojedynczej linii, a nie od całkowitego rozmiaru pliku — to największa wartość formatu liniowego.
3.1 Gdy potrzebujesz tylko kilku pól, nie wywołuj najpierw JSON.parse
Jeśli każdy rekord ma 50 pól, a potrzebujesz 3, JSON.parse buduje obiekty dla wszystkich 50.
Parser strumieniowy (stream-json, clarinet) potrafi wyciągnąć tylko docelowe pola, kosztem wyższej złożoności implementacyjnej.
Zasada praktyczna: gdy pojedynczy rekord ma poniżej kilkuset KB, bezpośrednie wywołanie JSON.parse jest zwykle wciąż lepszym interesem.
3.2 Chiński tekst i BOM
Pliki .jsonl generowane w Windows często niosą BOM, a JSON.parse na pierwszej linii z pewnością zawiedzie. Usuń BOM przed odczytem albo sprawdź pierwsze trzy bajty podczas strumieniowania pierwszego fragmentu.
4. Zapis: dopisywanie i atomowość
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' });
}
Dwie rzeczy do zapamiętania:
- Wyjście
JSON.stringifynie zawiera nowych linii (chyba że\nw łańcuchu jest escape'owane do dwóch znaków), więc jedna linia zawsze odpowiada dokładnie jednemu rekordowi; - Gdy kilka procesów dopisuje współbieżnie, pojedynczy
writemniejszy niż PIPE_BUF (zwykle 4096 bajtów na Linuksie) jest atomowy; wszystko większe nie ma gwarancji. W scenariuszach o wysokiej współbieżności przejdź na „jeden plik per proces, scalane po zrzuceniu na dysk” albo użyj magazynu z transakcjami.
5. Obsługa złych linii
To decyzja, którą format liniowy wymusza z góry:
| Strategia | Pasuje do | Koszt |
|---|---|---|
| Pomijanie i zliczanie | Danych tolerujących luki, takich jak logi i zdarzenia analityczne | Dane giną po cichu, więc alerty są obowiązkowe |
| Natychmiastowa porażka | Danych, których nie wolno zgubić, takich jak rozliczenia i faktury | Jedna zła linia blokuje całą partię |
| Przekierowanie do pliku dead-letter | Przypadków wymagających późniejszego uzupełnienia | Dodatkowe miejsce i proces odtwarzania |
Niezależnie od wyboru zapisz numer wiersza i fragment oryginalnej treści, inaczej później nie zlokalizujesz problemu. Numer wiersza nie występuje w komunikacie błędu JSON.parse, więc musisz utrzymywać go samodzielnie.
6. Kompresja i dzielenie na fragmenty
- gzip kompresuje
.jsonlw konsekwentnie wysokim stosunku (nazwy pól się powtarzają), więc zalecany jest.jsonl.gz; - gzip kompresuje blokami i natywnie wspiera strumieniową dekompresję, więc nie ma potrzeby dekompresowania całego pliku;
- podziel duże pliki na fragmenty o rozmiarze około
1GB, z numerami sekwencji w nazwach, aby ułatwić równoległe przetwarzanie i wznawialny transfer; - nigdy nie przetwarzaj
.jsonlprzez „scalanie do tablicy” — to wyrzuca wszystkie jego zalety.
7. Kompromisy względem innych formatów big data
| Format | Struktura | Zawiera schemat | Dobre do |
|---|---|---|---|
| JSON Lines | Niezależny per wiersz | Nie (wymaga zewnętrznego kontraktu) | Logi, importy i eksporty, dane treningowe |
| CSV | Tabelaryczny | Nie | Raporty, wymiana z Excelem |
| Parquet | Kolumnowy | Tak | Zapytania analityczne, bardzo duże zbiory offline |
| Avro | Wierszowy | Tak | Komunikaty Kafka, ewolucja schematu |
Zaletą JSON Lines jest to, że każdy język i każde narzędzie potrafi go natychmiast czytać i zapisywać; wadą jest brak schematu i kompresji kolumnowej. Gdy dane przestają mieścić się na jednej maszynie i potrzebne są częste zapytania kolumnowe, czas rozważyć Parquet.
8. Lista kontrolna
- [ ] Rozszerzenie pliku jest spójne (
.jsonllub.ndjson) i nigdy nie mieszane; - [ ] Writer gwarantuje „jeden rekord na wiersz”, a nowe linie w łańcuchach są już escape'owane;
- [ ] Reader usuwa BOM, pomija puste wiersze i utrzymuje numery wierszy;
- [ ] Strategia złych linii jest jawnie zdefiniowana i monitorowana (złe linie / wszystkie linie);
- [ ] Duże pliki są dzielone według rozmiaru i połączone z gzip oraz równoległym odczytem;
- [ ] Tam, gdzie istnieje kontrakt pól, dostarczany jest dodatkowy schemat i walidacja linia po linii.
FAQ
Jak wybrać między tablicą JSON a JSON Lines?
Spójrz, czy dane są partią rekordów, czy jednym dokumentem. Wybierz JSON Lines, gdy rekordy są dopisywane i czytane jako strumień: dopisywanie oznacza zapisanie jednej linii na końcu, a odczyt wymaga tylko podziału po wierszach. Zachowaj tablicę JSON, gdy całą strukturę trzeba zwalidować jednym przebiegiem, ponieważ tablica JSON waliduje wszystko naraz, a JSON Lines tylko linia po linii.
Dlaczego jedna uszkodzona linia nie psuje całego pliku?
Każda linia w JSON Lines jest niezależną, kompletną wartością JSON, a parsowanie jest naturalnie linia po linii, więc uszkodzone dane dotykają tylko tej linii. W tablicy JSON jedna błąd składni czyni cały plik nieparsowalnym, i właśnie dlatego dane liniowe pasują do logów i zdarzeń analitycznych.
Czy konwertować JSON Lines do tablicy JSON?
Można: czytaj linia po linii, wywołaj JSON.parse na każdym rekordzie i zbierz je do tablicy. Wymaga to jednak zmieszczenia całej treści w pamięci i rezygnuje ze strumieniowego odczytu oraz dopisywania jednej linii. Przetwarzanie .jsonl do tablicy wyrzuca wszystkie zalety formatu, więc jest rozsądne tylko przy jednorazowym dostarczeniu, gdy dane mieszczą się w pamięci.