JSONL
JSONL
JSONL (JSON Lines) — это текстовый формат, где каждая строка файла — самостоятельный JSON-объект, а строки разделены символом перевода строки
\n. Никаких внешних скобок, никаких запятых между записями.
История
Точную хронологию задокументировал Иэн Уорд (Ian Ward) на своём сайте jsonlines.org — там лежит односграничная спецификация, минимальная и насчитывающая всего четыре правила. По разным оценкам сайт появился около 2013 года; точную дату Уорд публично не подсвечивал, и я в ней не уверен — если важно, проверь по архивам web.archive.org.
Параллельно с jsonlines.org существовала и существует спека ndjson.org — «Newline Delimited JSON». Её начинал британский разработчик Финн Палмер (Finn Palmer) тоже в начале 2010-х. Технически jsonl и ndjson — один и тот же формат, просто два названия. В коде ты встретишь оба: расширения .jsonl, .ndjson, реже .jsonlines. Все парсеры обычно понимают все три.
Дальше шла «вторая волна». В середине 2010-х формат подхватили инструменты логирования: Fluentd, потом Vector от Datadog, потом Promtail (Loki). Им JSONL подошёл идеально — он stream-friendly, append-friendly, и парсится построчно. К концу 2010-х в облачных логах (Google Cloud Logging, AWS CloudWatch со structured logging, Cloudflare Logpush) JSONL стал де-факто стандартом.
Третья волна — 2022–2024 годы. OpenAI ввёл fine-tuning через JSONL-датасеты, потом Batch API — тоже только JSONL. Anthropic в Claude Code сохраняет логи сессий в JSONL (как раз файлы в ~/.claude/projects/*/*.jsonl, ради которых живёт твой daily-digest). HuggingFace datasets умеет JSONL «из коробки». Формат, который начинался как «надстройка над JSON для людей», стал общеиндустриальным интерфейсом для AI-пайплайнов.
Что это такое
Возьми обычный JSON и представь, что массив объектов вытащили наружу — не в виде квадратных скобок и запятых, а в виде физических строк файла. Получишь JSONL.
{"user":"alice","action":"login","ts":1700000000}
{"user":"bob","action":"logout","ts":1700000060}
{"user":"alice","action":"click","ts":1700000120,"page":"/pricing"}
vs тот же массив в обычном JSON:
[
{"user":"alice","action":"login","ts":1700000000},
{"user":"bob","action":"logout","ts":1700000060},
{"user":"alice","action":"click","ts":1700000120,"page":"/pricing"}
]
Разница на первый взгляд косметическая, но на самом деле меняется природа файла. JSON — это один документ с явным началом и явным концом. Пока ты не дочитал до закрывающей ], документ невалиден. JSONL — это поток независимых записей. Каждая строка существует сама по себе и парсится независимо от соседей.
Четыре правила формальной спеки (с jsonlines.org):
- Кодировка — UTF-8.
- Каждая строка — валидный JSON-объект.
- Разделитель —
\n(LF).\r\n(CRLF) допустим, парсеры обычно толерантны. - Расширение файла — рекомендовано
.jsonl.
Пустые строки внутри файла спека формально не разрешает, но большинство реальных парсеров их просто пропускает. Завершающий \n в конце файла — норма, отсутствие его тоже норма.
JSONL vs JSON-массив — главные пары различий:
- JSON массив надо распарсить целиком, чтобы получить первый объект. JSONL — построчно, первый объект готов после первой строки.
- В JSON-массив нельзя дописать запись без чтения и перезаписи всего файла (надо вставить запятую перед
]). В JSONL — просто>> file.jsonlи всё. - Битый JSON-массив теряет весь файл. Битая строка в JSONL — теряется одна запись.
JSONL vs CSV — тоже близкая пара. CSV — табличный, JSONL — вложенный. Если у тебя плоские записи с одинаковыми колонками, CSV компактнее. Если есть вложенность, массивы, отсутствующие поля, разные схемы у разных записей — CSV превращается в боль с экранированием и пустыми ячейками, а JSONL разгребается тривиально.
Аналогии из жизни
Стопка чеков из кассы. Каждый чек — отдельная бумажка с полной информацией: магазин, дата, товары, сумма. Кассовая лента наматывается, новый чек просто допечатывается в конец. Это JSONL. JSON-массив — это аналог квартального отчёта, где все продажи запихнуты в один сшитый и пронумерованный сборник.
Где ломается: На реальной кассовой ленте чеки идут строго по времени. В JSONL-файле порядок тоже сохраняется (это плюс), но если твой пайплайн пишет в один файл из нескольких потоков параллельно — порядок может смешаться, как если бы два кассира печатали на одну ленту.
Записные книжки vs книга. Книга в твёрдом переплёте — это JSON: оглавление, нумерация, чёткое начало и конец. Стопка карточек или дневников — это JSONL: каждая карточка самостоятельна, добавлять новые легко.
Где ломается: В книге всегда видно, сколько глав. В стопке карточек — посмотри глазами. Это значит, что на JSONL-файле нельзя «узнать длину» без полного прохода (или без дополнительного индекса).
Поезд из вагонов. JSONL — товарный состав, где каждый вагон-цистерна — это отдельный JSON-объект. Состав можно удлинить, прицепив ещё вагон сзади, или укоротить, отцепив. JSON-массив — пассажирский поезд с локомотивом, открывающей и закрывающей скобкой; пока не прицеплён локомотив, поезд не поедет.
Где ломается: В реальном поезде вагоны нумерованы и порядок жёсткий. В JSONL порядок строк сохраняется в самом файле, но никакой нумерации нет: если ты хочешь «найти 1000-ю запись», придётся пробежать первые 999 строк, индекса нет.
Как это работает
Алгоритм записи:
- Открываешь файл в режиме append (
aв Python,>>в shell). - Сериализуешь объект в JSON-строку — без переносов внутри.
- Дописываешь
\nв конце. - Закрываешь (или flush'ишь, если пишешь часто).
import json
event = {"user": "alice", "action": "login", "ts": 1700000000}
with open("events.jsonl", "a") as f:
f.write(json.dumps(event, ensure_ascii=False) + "\n")
Обрати внимание: json.dumps по умолчанию не добавляет переносы внутри объекта, поэтому одна запись = одна строка. Если в значениях встречается кириллица и хочется человекочитаемости, ставь ensure_ascii=False.
Алгоритм чтения:
- Открываешь файл построчно.
- Каждую непустую строку парсишь как самостоятельный JSON.
- Если строка битая — решаешь стратегию: бросить ошибку, пропустить, залогировать.
import json
with open("events.jsonl") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
event = json.loads(line)
process(event)
except json.JSONDecodeError:
# битая строка — обычно последняя при оборванном write
continue
В shell-мире главный инструмент — jq. Он умеет обрабатывать JSONL «из коробки»:
# каждую строку прогнать через jq и оставить как есть
jq -c '.' events.jsonl
# отфильтровать события Алисы
jq -c 'select(.user == "alice")' events.jsonl
# сгруппировать по пользователю
jq -s 'group_by(.user) | map({user: .[0].user, count: length})' events.jsonl
Флаг -c (compact) важен — он гарантирует, что вывод тоже будет JSONL (один объект на строку). Без него jq развернёт каждый объект в pretty-print, и формат поломается.
Streaming-парсер не нужен
Главная инженерная фишка JSONL: тебе не нужен потоковый JSON-парсер, чтобы стримить большие данные. Обычный построчный read работает на терабайтных файлах в 50 строк кода. С обычным JSON-массивом так не получится — пришлось бы либо тянуть весь файл в память, либо подключать SAX-подобный парсер (как ijson в Python). JSONL делегирует «стрим» уровню файла, а не уровню парсера.
Что под капотом, если ты пишешь стрим в файл из нескольких процессов:
- В POSIX write на маленький буфер (до
PIPE_BUF, обычно 4096 байт) атомарен. То есть две короткие записи не перемешаются между собой. - Если запись длиннее
PIPE_BUF— могут перемешаться. Тогда нужен либо flock, либо один writer-процесс, либо отдельные файлы на процесс.
Поэтому в логирующих пайплайнах часто стоит один collector-процесс (Fluentd, Vector), который принимает события по сети и пишет в один JSONL-файл — это и решает проблему конкурентной записи.
Где встречается в обычной жизни
- Apple Health экспорт. Если в приложении «Здоровье» нажать «Экспортировать всё», получишь XML, но многие сторонние утилиты (типа Health Auto Export) пишут в JSONL — каждое измерение пульса/шагов отдельной строкой.
- Логи мобильных приложений. Когда ты жалуешься в техподдержку Telegram или Notion и приложение «отправляет логи» — внутри это, скорее всего, JSONL-файл с событиями приложения.
- OAuth-аудит в Google/Microsoft. Если ты как админ компании выгружаешь историю входов сотрудников — экспорт часто приходит в JSONL.
- Yandex.Cloud Functions и AWS Lambda. Когда смотришь «structured logs» функции, в консоли — JSON-объекты построчно, в файле это JSONL.
- Twitter/X Archive. До закрытия API экспорт твоей истории твитов приходил в JSONL — каждый твит отдельной строкой.
Где встречается в IT и бизнесе
- Логирование. Fluentd, Vector, Promtail, Filebeat — все пишут JSONL. Datadog, Loki, Elasticsearch — принимают JSONL по HTTP. Это де-факто стандарт structured logging.
- ML обучающие выборки. OpenAI fine-tuning API принимает только JSONL (каждая строка — пара prompt/completion). Anthropic Workbench, HuggingFace datasets, Replicate — то же самое.
- Batch API. OpenAI Batch и Anthropic Message Batches принимают JSONL-файл с запросами, возвращают JSONL с ответами. Дешевле, чем стримить по одному.
- ETL и аналитика. BigQuery, Snowflake, Spark, dbt — все умеют грузить JSONL как источник. Часто промежуточный формат между «сырыми» данными и колонночным Parquet.
- Streaming API. Anthropic и OpenAI стримят ответы как SSE (Server-Sent Events), где каждое событие — JSON-объект. Если сохранить такой стрим в файл — получится JSONL.
Кто пользуется
- Anthropic. Claude Code хранит логи сессий в
~/.claude/projects/<project>/<session>.jsonl. Каждая строка — событие (user message, assistant message, tool call, tool result). Точные масштабы Anthropic не раскрывает, но если у активного пользователя сотни сессий — это сотни JSONL-файлов на машине. - OpenAI. Fine-tuning, Batch API — все интерфейсы для разработчиков работают через JSONL. Когда OpenAI летом 2024 запустил Batch API со скидкой 50% — формат стал стандартом для массового инференса.
- Google. Cloud Logging пишет JSONL в storage-бакеты при экспорте. BigQuery импортирует JSONL напрямую без преобразования. Точные цифры не знаю, но по разным оценкам Google Logging обрабатывает миллионы JSONL-строк в секунду.
- Cloudflare. Logpush — стриминг логов из Cloudflare во внешнее хранилище — отдаёт данные в JSONL (опционально CSV).
- HuggingFace. Стандартный формат datasets, доступен через
load_dataset("path", data_files="*.jsonl"). - GitHub. API возвращает обычный JSON, но при больших экспортах (Migration API) — JSONL.
Альтернативы и конкуренты
- CSV. Плюс: проще, открывается в Excel/Numbers. Минус: вложенные структуры — кошмар, экранирование запятых и кавычек требует понимания диалекта (RFC 4180, Excel, MySQL — все немного разные).
- Parquet. Плюс: колонночное хранение, сжатие в 5–20 раз лучше JSONL, мгновенные агрегации по колонке. Минус: бинарный, руками не редактируется, требует библиотеку (pyarrow, Apache Parquet) для чтения.
- Avro. Плюс: схема хранится внутри файла, эволюция схемы (новые поля) поддерживается «из коробки», бинарный. Минус: не читается без библиотеки, реже встречается в обычных продуктах.
- MessagePack / CBOR. Плюс: компактный двоичный аналог JSON, экономит 30–50% размера. Минус: бинарный, нет встроенного потокового разделителя — нужно или длину писать в начало, или newline-аналог придумывать.
- Protocol Buffers (Protobuf). Плюс: типизированный, очень компактный, версионируемый. Минус: схема обязательна, бинарный, для логов чрезмерно сложен.
Практический выбор: если данные «текут» в файл и читаются человеком или скриптом — JSONL. Если хранятся надолго и анализируются аналитически — Parquet. Если межсервисный API с жёсткой схемой — Protobuf. Excel-friendly выгрузка для не-инженеров — CSV.
Когда НЕ стоит использовать
- Когда нужен случайный доступ по индексу. JSONL читается только последовательно. Если тебе часто нужна «строка номер N» или «все записи где user=X», без полного прохода — бери SQLite, Parquet с индексом или нормальную БД.
- Когда хранилище ограничено и данные долгоживущие. JSONL — текстовый, без сжатия. Тот же набор записей в Parquet с zstd-сжатием займёт в 10–20 раз меньше. На терабайтных архивах это десятки тысяч долларов в год разницы.
- Когда нужна гарантия схемы. В JSONL каждая строка может иметь свою структуру — это и сила, и слабость. Если ты делаешь стабильный межсервисный контракт — лучше Avro/Protobuf/JSON Schema с валидацией, иначе через год получишь «мусорное поле» в каждой пятой записи.
JSONL не self-describing
В JSONL-файле нет «заголовка», который говорил бы, какие поля в каждой записи. CSV хотя бы первой строкой даёт колонки. JSONL — нет. Это значит, что если ты получил events.jsonl без документации — узнать, что внутри, можно только прочитав несколько строк. Для собственных пайплайнов это нормально, но для публичных датасетов обязательно прикладывай README или JSON Schema рядом.
Связанные понятия
- JSON Schema — формальное описание структуры JSON-объекта; можно валидировать каждую строку JSONL против схемы.
- NDJSON — синоним JSONL по факту, чуть другая спека на ndjson.org.
- CSV — табличный текстовый аналог; работает только на плоских данных.
- Server-Sent Events (SSE) — HTTP-протокол стриминга, переносящий по сути JSONL по сети с префиксом
data:. - Parquet — колонночный бинарный формат для аналитики на больших объёмах.
- jq — главный CLI-инструмент для обработки JSON/JSONL в терминале.
Литература и источники
- jsonlines.org — официальная спецификация JSONL, одна страница.
- ndjson.org — спецификация NDJSON, формально другой стандарт, де-факто тот же формат.
- «Designing Data-Intensive Applications», Мартин Клеппман, 2017 (en) — глава 4 «Encoding and Evolution» разбирает форматы сериализации от JSON и CSV до Avro и Protobuf, объясняет, когда какой брать.
- Wikipedia: «JSON streaming» (en) — обзорная статья, перечисляет JSONL, NDJSON, Concatenated JSON, Length-prefixed JSON.
- jq manual на stedolan.github.io/jq/manual — главный практический гайд по работе с JSONL в терминале.
- OpenAI документация по fine-tuning на platform.openai.com — пример «индустриального» применения JSONL как контрактного интерфейса.
Где встретилось у меня
В пайплайне daily-digest скрипт parse_jsonl.py читает все файлы ~/.claude/projects/*/*.jsonl — логи сессий Claude Code за вчера — и собирает из них контекст для статьи. Каждая строка такого файла — отдельное событие сессии: сообщение пользователя, ответ ассистента, вызов инструмента. Именно из-за того, что Claude Code пишет в JSONL (а не в обычный JSON), я могу спокойно дописывать события в течение дня и тут же читать вчерашние логи построчно без риска повредить структуру файла.
Краткое резюме
- JSONL = один валидный JSON-объект на строку, разделитель
\n, кодировка UTF-8. - Append-only и stream-friendly «по дизайну»: дописывать в конец легко, читать построчно — тривиально, оборванная строка не ломает остальные.
- Де-факто стандарт structured logging (Fluentd, Loki, Datadog) и AI-пайплайнов (OpenAI fine-tune/batch, Anthropic Claude Code, HuggingFace).
- Не подходит, когда нужен случайный доступ, агрессивное сжатие или строгая схема — там лучше Parquet, Avro или БД.
- В работе с AI-инструментами JSONL — это «общий язык» сохранения сессий и батчей, понимать его устройство полезно даже не-инженеру.