JSONL

22 июня 2026 · ~11 мин чтения

формат-данных json логи стриминг etl

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):

  1. Кодировка — UTF-8.
  2. Каждая строка — валидный JSON-объект.
  3. Разделитель — \n (LF). \r\n (CRLF) допустим, парсеры обычно толерантны.
  4. Расширение файла — рекомендовано .jsonl.

Пустые строки внутри файла спека формально не разрешает, но большинство реальных парсеров их просто пропускает. Завершающий \n в конце файла — норма, отсутствие его тоже норма.

JSONL vs JSON-массив — главные пары различий:

JSONL vs CSV — тоже близкая пара. CSV — табличный, JSONL — вложенный. Если у тебя плоские записи с одинаковыми колонками, CSV компактнее. Если есть вложенность, массивы, отсутствующие поля, разные схемы у разных записей — CSV превращается в боль с экранированием и пустыми ячейками, а JSONL разгребается тривиально.

Аналогии из жизни

Стопка чеков из кассы. Каждый чек — отдельная бумажка с полной информацией: магазин, дата, товары, сумма. Кассовая лента наматывается, новый чек просто допечатывается в конец. Это JSONL. JSON-массив — это аналог квартального отчёта, где все продажи запихнуты в один сшитый и пронумерованный сборник.

Где ломается: На реальной кассовой ленте чеки идут строго по времени. В JSONL-файле порядок тоже сохраняется (это плюс), но если твой пайплайн пишет в один файл из нескольких потоков параллельно — порядок может смешаться, как если бы два кассира печатали на одну ленту.

Записные книжки vs книга. Книга в твёрдом переплёте — это JSON: оглавление, нумерация, чёткое начало и конец. Стопка карточек или дневников — это JSONL: каждая карточка самостоятельна, добавлять новые легко.

Где ломается: В книге всегда видно, сколько глав. В стопке карточек — посмотри глазами. Это значит, что на JSONL-файле нельзя «узнать длину» без полного прохода (или без дополнительного индекса).

Поезд из вагонов. JSONL — товарный состав, где каждый вагон-цистерна — это отдельный JSON-объект. Состав можно удлинить, прицепив ещё вагон сзади, или укоротить, отцепив. JSON-массив — пассажирский поезд с локомотивом, открывающей и закрывающей скобкой; пока не прицеплён локомотив, поезд не поедет.

Где ломается: В реальном поезде вагоны нумерованы и порядок жёсткий. В JSONL порядок строк сохраняется в самом файле, но никакой нумерации нет: если ты хочешь «найти 1000-ю запись», придётся пробежать первые 999 строк, индекса нет.

Как это работает

Алгоритм записи:

  1. Открываешь файл в режиме append (a в Python, >> в shell).
  2. Сериализуешь объект в JSON-строку — без переносов внутри.
  3. Дописываешь \n в конце.
  4. Закрываешь (или 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.

Алгоритм чтения:

  1. Открываешь файл построчно.
  2. Каждую непустую строку парсишь как самостоятельный JSON.
  3. Если строка битая — решаешь стратегию: бросить ошибку, пропустить, залогировать.
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 делегирует «стрим» уровню файла, а не уровню парсера.

Что под капотом, если ты пишешь стрим в файл из нескольких процессов:

Поэтому в логирующих пайплайнах часто стоит один collector-процесс (Fluentd, Vector), который принимает события по сети и пишет в один JSONL-файл — это и решает проблему конкурентной записи.

Где встречается в обычной жизни

Где встречается в IT и бизнесе

Кто пользуется

Альтернативы и конкуренты

Практический выбор: если данные «текут» в файл и читаются человеком или скриптом — JSONL. Если хранятся надолго и анализируются аналитически — Parquet. Если межсервисный API с жёсткой схемой — Protobuf. Excel-friendly выгрузка для не-инженеров — CSV.

Когда НЕ стоит использовать

JSONL не self-describing

В JSONL-файле нет «заголовка», который говорил бы, какие поля в каждой записи. CSV хотя бы первой строкой даёт колонки. JSONL — нет. Это значит, что если ты получил events.jsonl без документации — узнать, что внутри, можно только прочитав несколько строк. Для собственных пайплайнов это нормально, но для публичных датасетов обязательно прикладывай README или JSON Schema рядом.

Связанные понятия

Литература и источники

Где встретилось у меня

В пайплайне daily-digest скрипт parse_jsonl.py читает все файлы ~/.claude/projects/*/*.jsonl — логи сессий Claude Code за вчера — и собирает из них контекст для статьи. Каждая строка такого файла — отдельное событие сессии: сообщение пользователя, ответ ассистента, вызов инструмента. Именно из-за того, что Claude Code пишет в JSONL (а не в обычный JSON), я могу спокойно дописывать события в течение дня и тут же читать вчерашние логи построчно без риска повредить структуру файла.

Краткое резюме