JSON Toolkit

JSON Lines と NDJSON:ストリーミングデータ

要点:1 行 1 JSON 値にすると、追記可能・ストリーミング可能・破損が 1 行に閉じるという利点が得られます。代償は全体構造の欠如で、検証は行単位で行う必要があります。

中級約 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 のウェブサイトは「1 行 1 値で、値は任意の JSON(スカラーを含む)でよい」ことを強調し、 NDJSON 仕様は「空行なし、トップレベルスカラーなし」といったより厳格な実装規則を加えています。実務では厳格な方に従えばトラブルを避けられます。

2. JSON 配列との比較

観点 JSON 配列 JSON Lines
レコードを 1 件追記 ファイル全体を書き換える(閉じる ] を変える) 1 行追記するだけ
ストリーミング読み取り 括弧の深さを追うストリーミングパーサーが必要 行で分割するだけ
1 件の破損 ファイル全体がパース不能になる その行だけが影響を受ける
ファイル全体の検証 一度にすべて検証できる 行単位でしかできない
圧縮 gzip 後は同程度のサイズ 同じ。ブロック単位で並列圧縮できる
ネストの表現 ネイティブにサポート 各行内ではネスト可能。行同士は無関係
典型的な利用者 すべての JSON ツール ログシステム、ビッグデータパイプライン、LLM 学習データ

結論は明快です。データが「レコードの集まり」か「一つの文書」かで形式が決まります。

3. 読み取り:行単位のストリーミング

Node では readline が最も簡単です。

import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';

/**
 * JSON Lines ファイルを行ごとに読み、レコードごとにコールバックを呼ぶ。
 * @param filePath ファイルのパス
 * @param onRecord 1 レコードの処理関数
 * @returns 成功行数と失敗行数を含む統計
 */
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 };
}

このコードのメモリ使用量は 1 行のサイズだけに依存し、ファイル全体のサイズには依存しません。これが行区切り形式の最大の価値です。

3.1 一部のフィールドだけ必要なら、まず JSON.parse しない

各レコードが 50 フィールドあり、必要なのが 3 つだけなら、JSON.parse は 50 個すべてのオブジェクトを構築します。 ストリーミングパーサー(stream-json、clarinet)は対象フィールドだけを抽出できますが、実装の複雑さが増します。 経験則:1 レコードが数百 KB 未満なら、JSON.parse を直接呼ぶ方が通常は割に合います。

3.2 中国語テキストと BOM

Windows で生成された .jsonl ファイルはしばしば BOM を伴い、最初の行の JSON.parse は必ず失敗します。読み取り前に BOM を除去するか、最初のチャンクをストリーミングするときに先頭 3 バイトを検査してください。

4. 書き込み:追記と原子性

import { appendFile } from 'node:fs/promises';

/**
 * JSON Lines レコードを 1 件追記する。
 * @param filePath 対象ファイル
 * @param record 書き込むレコードオブジェクト
 * @returns 書き込み完了時に解決する Promise
 */
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 文字にエスケープされる場合を除く)ので、1 行は常に正確に 1 レコードです。
  2. 複数プロセスが同時に追記する場合、PIPE_BUF(Linux では通常 4096 バイト)より小さい単一の write は原子的ですが、それより大きい場合は保証されません。 高並列の場面では「プロセスごとに 1 ファイル、ディスクに書き出した後に統合」に切り替えるか、トランザクションを持つストレージを使ってください。

5. 壊れた行の扱い

これは行区切り形式が事前に決めさせてくる判断です。

戦略 向いているデータ コスト
スキップして数える 欠落を許容するデータ(ログ、分析イベント) データが静かに失われるので、アラートが必須
即座に失敗する 失ってはならないデータ(請求、帳票) 1 行の破損がバッチ全体を止める
デッドレターファイルへ回す 後で埋め戻しが必要な場合 追加のストレージと再実行の仕組み

どれを選んでも、行番号と元の内容の断片を記録してください。さもないと後から問題を特定できません。行番号は JSON.parse のエラーメッセージに含まれないので、自分で維持する必要があります。

6. 圧縮と分割

  • gzip は .jsonl を安定して高い比率で圧縮します(フィールド名が繰り返されるため)。.jsonl.gz がおすすめです。
  • gzip はブロック単位で圧縮し、ストリーミング解凍をネイティブに支えるので、ファイル全体を解凍する必要はありません。
  • 大きなファイルはおよそ 1GB のスライスに分割し、名前に連番を入れると並列処理とレジューム転送が楽になります。
  • .jsonl を「配列に統合する」前処理は絶対にしないでください。利点がすべて失われます。

7. 他のビッグデータ形式とのトレードオフ

形式 構造 スキーマを含む 向いている用途
JSON Lines 行ごとに独立 なし(外部の契約が必要) ログ、インポート・エクスポート、学習データ
CSV 表形式 なし レポート、Excel 連携
Parquet 列指向 あり 分析クエリ、非常に大規模なオフラインデータ
Avro 行指向 あり Kafka メッセージ、スキーマ進化

JSON Lines の利点はどの言語、どのツールでもすぐに読み書きできること。欠点はスキーマと列指向圧縮がないことです。 データが 1 台のマシンに収まらなくなり、列指向のクエリを頻繁に行う必要が出たら、Parquet を検討する時です。

8. チェックリスト

  • [ ] 拡張子は統一されている(.jsonl か .ndjson)。混在させない。
  • [ ] 書き込み側が「1 行 1 レコード」を保証し、文字列内の改行はエスケープ済み。
  • [ ] 読み取り側が BOM を除去し、空行をスキップし、行番号を維持する。
  • [ ] 壊れた行の戦略が明示され、監視されている(破損行数 / 総行数)。
  • [ ] 大きなファイルはサイズで分割し、gzip と並列読み取りを組み合わせている。
  • [ ] フィールド契約がある場合は追加のスキーマを用意し、行単位で検証する。

FAQ

JSON 配列と JSON Lines はどう選べばよいですか?

データがレコードの集まりか、単一の文書かで判断します。レコードを追記し、ストリームとして読むなら JSON Lines を選びます。追記は末尾に 1 行書くだけで、読み取りは行分割だけで済みます。全体構造を一度に検証する必要があるなら JSON 配列のままにします。JSON 配列はすべてを一度に検証できますが、JSON Lines は行単位でしか検証できないからです。

壊れた行が 1 つあってもファイル全体が壊れないのはなぜですか?

JSON Lines の各行は独立した完全な JSON 値で、パースは自然に行単位です。ですから破損データはその行だけに影響します。JSON 配列では構文エラーが 1 つあるとファイル全体がパース不能になり、まさにそれが行区切りデータがログや分析イベントに向く理由です。

JSON Lines を JSON 配列に変換すべきですか?

できます。行ごとに読み、各レコードに JSON.parse を呼び、配列に集めます。ただし全内容がメモリに収まる必要があり、ストリーミング読み取りと 1 行追記の利点を失います。.jsonl を配列に前処理すると形式の利点がすべて失われるので、データがメモリに収まる一度きりの受け渡しに限って妥当です。