JSON Lines и NDJSON: потоковые данные
Главное: Одно значение JSON на строку даёт вам дозаписываемость, потоковость и изоляцию ущерба одной строкой; цена — отсутствие общей структуры, поэтому валидация должна происходить построчно.

Главное: используйте массив JSON, когда обрабатываете весь пакет сразу, и JSON Lines, когда нужны потоковые дозаписи и изоляция плохих строк; если вам всё ещё нужно валидировать общую структуру, не переходите на формат с разделением по строкам.
Как только данные превышают память или продолжают поступать непрерывно, структура «один большой массив» становится врагом.
1. Правила формата
JSON Lines (.jsonl) и NDJSON (.ndjson) практически синонимы, и правила кратки в обоих случаях:
- Кодировка UTF-8, без BOM;
- Каждая строка содержит одно полное, валидное значение JSON;
- Строки разделяются
\n; последняя строка может или не может заканчиваться переводом строки; - Внутри строки не может появляться неэкранированный перевод строки — перевод строки внутри строки должен быть записан как
\n; - Пустые строки обычно игнорируются, но строгая реализация должна трактовать их как невалидные.
{"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"}
Тонкое различие между ними: сайт JSON Lines подчёркивает «одно значение на строку, и значение может быть любым JSON (включая скаляры)», тогда как спецификация NDJSON добавляет более строгие правила реализации, такие как «без пустых строк и без скаляров верхнего уровня». В инженерии следование более строгому набору избавляет от проблем.
2. Сравнение с массивом JSON
| Аспект | Массив JSON | JSON Lines |
|---|---|---|
| Дозаписать одну запись | Переписать весь файл (изменить закрывающую ]) |
Просто дозаписать одну строку |
| Потоковое чтение | Нужен потоковый парсер, отслеживающий глубину скобок | Разделение по строкам |
| Одна испорченная запись | Весь файл становится неразбираемым | Затронута только та строка |
| Валидация всего файла | Валидировать всё за один проход | Только построчно |
| Сжатие | Похожий размер после gzip | То же, и блоки можно сжимать параллельно |
| Выражение вложенности | Поддерживается нативно | Каждая строка может быть вложенной внутри; строки не связаны друг с другом |
| Типичные потребители | Все инструменты JSON | Системы логов, конвейеры больших данных, наборы обучающих данных LLM |
Вывод прямой: являются ли данные «пакетом записей» или «одним документом», решает, какой формат использовать.
3. Чтение: потоковая обработка построчно
В Node самый простой подход — 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 };
}
Использование памяти этого кода зависит только от размера одной строки, а не от общего размера файла — это наибольшая ценность формата с разделением по строкам.
3.1 Когда нужно только несколько полей, не вызывайте JSON.parse первым
Если каждая запись имеет 50 полей, а вам нужны только 3, JSON.parse строит объекты для всех 50.
Потоковый парсер (stream-json, clarinet) может извлекать только целевые поля ценой более высокой сложности реализации.
Правило большого пальца: когда одна запись меньше нескольких сотен КБ, прямой вызов JSON.parse обычно всё ещё выгоднее.
3.2 Китайский текст и BOM
Файлы .jsonl, созданные в Windows, часто несут BOM, и JSON.parse на первой строке гарантированно падает. Удалите BOM перед чтением или проверьте первые три байта при потоковой обработке первого фрагмента.
4. Запись: дозаписи и атомарность
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' });
}
Две вещи, на которые стоит обратить внимание:
- Вывод
JSON.stringifyне содержит переводов строк (если только\nстроки не экранирован в два символа), поэтому одна строка всегда ровно одна запись; - Когда несколько процессов дозаписывают одновременно, одна запись
writeменьше PIPE_BUF (обычно 4096 байт в Linux) атомарна; для всего большего это не гарантировано. Для сценариев с высокой конкурентностью перейдите на «один файл на процесс, объединённые после сброса на диск» или используйте хранилище с транзакциями.
5. Обработка плохих строк
Это решение, которое формат с разделением по строкам заставляет вас принять заранее:
| Стратегия | Подходит для | Цена |
|---|---|---|
| Пропустить и посчитать | Данные, допускающие пробелы, например логи и события аналитики | Данные теряются молча, поэтому оповещение обязательно |
| Падать сразу | Данные, которые нельзя терять, например биллинг и счета | Одна плохая строка блокирует весь пакет |
| Направить в файл мёртвых писем | Случаи, требующие последующего восстановления | Дополнительное хранилище и процесс воспроизведения |
Что бы вы ни выбрали, записывайте номер строки и фрагмент исходного содержимого, иначе вы не сможете найти проблему потом. Номера строки нет в сообщении об ошибке JSON.parse, поэтому вы должны поддерживать его сами.
6. Сжатие и шардирование
- gzip сжимает
.jsonlстабильно с высоким коэффициентом (имена полей повторяются), поэтому рекомендуется.jsonl.gz; - gzip сжимает блоками и нативно поддерживает потоковую распаковку, поэтому нет нужды распаковывать весь файл;
- Разбивайте большие файлы на срезы примерно по
1GB, с порядковыми номерами в именах, чтобы облегчить параллельную обработку и возобновляемую передачу; - Никогда не предобрабатывайте
.jsonl, «сливая его в массив» — это отбрасывает все его преимущества.
7. Компромиссы против других форматов больших данных
| Формат | Структура | Включена ли схема | Хорош для |
|---|---|---|---|
| JSON Lines | Независима на строку | Нет (нужен внешний контракт) | Логи, импорт и экспорт, обучающие данные |
| CSV | Табличный | Нет | Отчёты, обмен с Excel |
| Parquet | Колоночный | Да | Аналитические запросы, очень большие офлайн-наборы данных |
| Avro | Строчный | Да | Сообщения Kafka, эволюция схемы |
Преимущество JSON Lines — любой язык и любой инструмент может читать и писать его немедленно; недостаток — отсутствие схемы и колоночного сжатия. Как только данные больше не помещаются на одной машине и вам нужны частые колоночные запросы, пора рассматривать Parquet.
8. Контрольный список
- [ ] Расширение файла согласовано (
.jsonlили.ndjson) и никогда не смешивается; - [ ] Писатель гарантирует «одна запись на строку», переводы строк внутри строк уже экранированы;
- [ ] Читатель удаляет BOM, пропускает пустые строки и поддерживает номера строк;
- [ ] Стратегия плохих строк явно определена и отслеживается (плохие строки / всего строк);
- [ ] Большие файлы нарезаны по размеру и дополнены gzip и параллельным чтением;
- [ ] Где существует контракт полей, предоставляется дополнительная схема и валидация построчно.
Вопросы и ответы
Как выбрать между массивом JSON и JSON Lines?
Посмотрите, являются ли данные пакетом записей или одним документом. Выбирайте JSON Lines, когда записи дозаписываются и читаются потоково: дозапись означает запись одной строки в конец, а чтение требует только разделения по строкам. Держите массив JSON, когда всю структуру нужно валидировать за один проход, потому что массив JSON валидирует всё сразу, а JSON Lines может валидироваться только построчно.
Почему одна испорченная строка не ломает весь файл?
Каждая строка в JSON Lines — независимое, полное значение JSON, а парсинг естественно построчный, поэтому испорченные данные влияют только на ту строку. В массиве JSON одна синтаксическая ошибка делает весь файл неразбираемым, что и является причиной, по которой данные с разделением по строкам подходят для логов и событий аналитики.
Стоит ли конвертировать JSON Lines в массив JSON?
Можно: читайте построчно, вызывайте JSON.parse для каждой записи и собирайте их в массив. Это требует, чтобы всё содержимое помещалось в память, и отказывается от потокового чтения и дозаписи одной строкой. Предобработка .jsonl в массив отбрасывает все преимущества формата, поэтому разумна только для разовой доставки, где данные помещаются в память.