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 у масив відкидає всі переваги формату, тож це розумно лише для одноразової доставки, де дані вміщуються в пам'ять.