JSON Toolkit

JSON Lines и NDJSON: потоковые данные

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

Средний~10 мин чтенияОпубликовано 2026-10-30Обновлено 2026-10-30
Обложка для JSON Lines и NDJSON потоковых данных

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

Как только данные превышают память или продолжают поступать непрерывно, структура «один большой массив» становится врагом.

1. Правила формата

JSON Lines (.jsonl) и NDJSON (.ndjson) практически синонимы, и правила кратки в обоих случаях:

  1. Кодировка UTF-8, без BOM;
  2. Каждая строка содержит одно полное, валидное значение JSON;
  3. Строки разделяются \n; последняя строка может или не может заканчиваться переводом строки;
  4. Внутри строки не может появляться неэкранированный перевод строки — перевод строки внутри строки должен быть записан как \n;
  5. Пустые строки обычно игнорируются, но строгая реализация должна трактовать их как невалидные.
{"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' });
}

Две вещи, на которые стоит обратить внимание:

  1. Вывод JSON.stringify не содержит переводов строк (если только \n строки не экранирован в два символа), поэтому одна строка всегда ровно одна запись;
  2. Когда несколько процессов дозаписывают одновременно, одна запись 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 в массив отбрасывает все преимущества формата, поэтому разумна только для разовой доставки, где данные помещаются в память.