JSONL ファイル実践:開き方・変換・壊れた行の調査
要点:.jsonl 全体を JSON.parse に渡してエラーが出るのは、ファイルが壊れているからではなく、読み方が違うからです。この記事では、確認と行単位読み取りの標準パターン、JSON 配列との相互変換コマンド、6 種類の壊れた行の対処表、ストリーミング API で NDJSON を消費する正しい書き方を解説します。

結論:JSONL の各行は独立した正当な JSON ですが、ファイル全体は 1 つの JSON ではありません。全体を JSON.parse や json.load に渡して失敗する原因は、十中八九ファイルの破損ではなく読み取り方の間違いです。
1 行 1 レコード、行間にカンマなし、外側の配列なし。JSONL は「全体構造を手放す」ことで、追記書き込み・ストリーミング読み取り・1 行だけの障害分離という 3 つの利点を手に入れます。この利点はすべて「行単位で処理する」ことで初めて実現し、「ファイル全体を一気にパースする」と一斉にエラーに変わります。
1. 3 つの名前、1 つの形式:JSONL と NDJSON と JSON Lines
初心者は名前の違いを文法の違いと考えがちですが、そうではありません。
| 名前 | よく使われる場面 | 拡張子 / メディアタイプ |
|---|---|---|
| JSON Lines | データエンジニアリング、ML データセット(pandas、Spark、Hugging Face) | .jsonl |
| NDJSON | HTTP ストリーミング API、リアルタイムデータ | .ndjson、application/x-ndjson |
| JSONL | どちらの略称としても使われる通称 | — |
片方を読めるツールはもう片方も読めます。名前を選ぶ基準は下流エコシステムの慣習だけです。Hugging Face のデータセットに渡すなら .jsonl、ストリーミング API を提供するなら application/x-ndjson を指定します。移植性の高い JSONL ファイルは 3 つのルールを満たします。UTF-8 エンコード(BOM なし)、1 行に 1 つの完全な JSON 値、行は \n で区切る。ファイル末尾の改行は強く推奨され、空行は正当なレコードではありません。プロトコル面の詳細と設計意図は JSON Lines と NDJSON の概説 に譲り、この記事は「ファイルを手にした後に何をするか」に限定します。
2. 確認の作法:まず head、次に grep、エディタは最後
500 MB の .jsonl をダブルクリックでエディタに開くのが最もよくある失敗です。300 万行のテキストにシンタックスハイライトと Undo バッファが乗ると、ほぼすべてのエディタがフリーズします。これは JSON のせいではありません。45 MB の文書のパース自体は Node 22 で約 208 ミリ秒。固まるのは描画です。レコードと物理行が揃っている行指向形式は、行ツールのためにあると言えます。
# まず規模を掴む:内容を読まずにレコード数とファイルサイズを確認
wc -l events.jsonl && du -h events.jsonl
# レコードの形を見る(フィールドと構造の確認)
head -n 1 events.jsonl | jq .
# 目的のレコードを直接探す:行ツールがそのまま使えるのは JSONL の特権
grep '"level":"error"' events.jsonl | head -n 5
tail -n 100 events.jsonl | jq 'select(.latency_ms > 500)'
最後のコマンドに注目してください。tail から出てくる各行は依然として独立した JSON 値なので、配列を作り直さずにそのまま jq に流せます。この方法は数 GB のファイルでもメモリ負担ほぼゼロで同じように機能します。これは 大きな JSON がページを遅くする理由 で説明している原理と同じです。
3. 行単位読み取りの 3 つの標準パターン
行単位の読み取りで扱うべきは 3 点です。空行のスキップ(特に末尾の改行)、行番号付きのエラー報告、そして 1 行の壊れで全体を止めないこと。3 つの言語の標準パターンはいずれもこの 3 点を中心に組み立てられています。
Python、標準ライブラリのみ:
import json
with open("events.jsonl", encoding="utf-8") as f:
for line_number, line in enumerate(f, start=1):
if not line.strip(): # 空行をスキップ
continue
try:
record = json.loads(line) # 各行を独立にパース
except json.JSONDecodeError as e:
print(f"行 {line_number} で失敗: {e}") # 報告するが中断しない
continue # 壊れた行を切り離す
handle(record)
Node は内蔵の readline モジュールで、ファイルサイズに関係なく一定メモリ:
import fs from "node:fs";
import readline from "node:readline";
const input = fs.createReadStream("events.jsonl", "utf8");
const lines = readline.createInterface({ input, crlfDelay: Infinity });
let lineNumber = 0;
for await (const line of lines) {
lineNumber += 1;
if (!line.trim()) continue;
try {
handle(JSON.parse(line));
} catch (e) {
console.error(`行 ${lineNumber}: ${e.message}`);
}
}
Go 1.27 では encoding/json/v2 が安定版のデフォルトになり、値単位のストリームデコードが JSONL にそのまま合います。jq 式を埋め込みたい場合は gojq を使います。ライブラリ選びで安易にパッケージを増やさないでください。Python の jsonlines は失敗行の行番号と生テキストを報告し、orjsonl は gzip / bzip2 / zstd 圧縮ファイルを直接読み書きできます。Node の ndjson パッケージは 2020 年以降リリースがなく、内蔵 readline で十分です。
4. JSON 配列との相互変換
方向を間違えるとすべてのコマンドが逆になります。覚えるべきルールは 1 つ。**配列 → JSONL は map(各レコードを 1 行に圧縮)、JSONL → 配列は collect(行を 1 つのリストに集める)**です。
# JSON 配列 → JSONL:-c で各要素を 1 行に圧縮
jq -c '.[]' records.json > records.jsonl
# JSONL → JSON 配列:-s で全入力を 1 つの配列にまとめる
jq -s '.' events.jsonl > records.json
# JSONL を行単位でフィルタ:-s は不要、jq は入力をストリームとして処理する
jq -c 'select(.active == true)' events.jsonl > active.jsonl
Python のワンライナーも等価です。配列 → JSONL はレコードごとに json.dumps(r, ensure_ascii=False) + "\n" を書き、逆方向は行ごとに json.loads してリストに集めます。それぞれの方向に落とし穴が 1 つずつあります。行く方向(配列 → JSONL)では、レコード内の実改行が json.dumps によって必ずエスケープされている必要があります。さもないと 1 レコードが複数行に裂けます。戻る方向(JSONL → 配列)では、行を連結して巨大な 1 文字列にしてからパースしないでください。数百万行ならメモリ上の無駄な全量コピー 1 回分です。集計(合計・グループ化・平均)には全量を読む -s が必要で、数 GB 級では awk か Python のストリーミング集計に切り替えます。
5. 壊れた行の 6 種類と調査手順
JSONL のエラーはほぼすべて行レベルで発生します。行単位で検証して行番号を報告するのが唯一の確実な診断です。頻度順に並べます。
| 症状 | 根本原因 | 修正 |
|---|---|---|
行末で Extra data |
行間にカンマを入れてしまった | 削除する。各行は単独で完結する必要がある |
| 1 レコードが複数行に分裂 | レコード内の実改行がエスケープされていない(手作業で整形) | json.dumps / jq -c で再圧縮 |
1 行目だけ Unexpected token ï で失敗 |
ファイルに BOM がある | BOM なし UTF-8 で保存し直す |
| 最終行だけエラー、中身は正しそう | 末尾改行の欠落や混入した空白 | \n を追記。慣習として空行はスキップ |
| 文字列内の改行でエラー | 文字列リテラル内に生の改行(\n ではない) |
生成側を修正する。JSON 文字列を手で連結しない |
| 大きな整数の値が変わる | 倍精度浮動小数点経由でパース(2^53 で切捨て) | 精度を保つパーサーを使う。BigInt 精度 を参照 |
診断の手順は常に 3 ステップです。まず head -n 1 でエンコードと先頭行の構造を確認し、次に最初の壊れで止まらず全壊れ行の行番号を報告する行単位スクリプトを実行し、最後に壊れた行を 1 行ずつ取り出して個別に調べます。1 行は 1 完全レコードなので、単独で調べるコストはほぼゼロです。一括取り込みでは「中止ではなく隔離」を推奨します。壊れた行と行番号を errors.jsonl に書き出し、正常行は通常どおり取り込み、壊れ行の比率を見て再取り込みを判断します。
6. HTTP 上の JSONL:行単位の消費とバックプレッシャー
HTTP 上の NDJSON の典型的な形は、サーバーが書き続け、クライアントが読み続ける構図です。クライアント側で response.json() を呼んではいけません。これはボディ全体をバッファしてからパースするため、ストリーミングの意味を消します。ボディストリームを直接読み、\n で分割し、末尾の未完成断片を次の反復へ持ち越します。
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const lines = buffer.split("\n");
buffer = lines.pop(); // 最後のチャンクは行の途中の可能性がある
for (const line of lines) {
if (line.trim()) handle(JSON.parse(line));
}
}
サーバー側では 2 点が必須です。第一に res.write() が false を返したら drain イベントを待つ(バックプレッシャー)。これを省くと消費者が生産者より遅いとき、メモリが際限なく増えます。第二にストリーミング経路で gzip ミドルウェアを無効にする。gzip は圧縮前にレスポンス全体をバッファする必要があり、NDJSON のストリーミングを完全に無効化します。どちらもローカル検証と複数の独立した英語情報源で裏付けを取った挙動であり、推測ではありません。
7. JSONL を選ぶべきでない場面
形式選定のネガティブスペースも重要です。次の 3 つは JSON 配列に留まるべきです。
| 要件 | JSONL が合わない理由 | 代わりに使うもの |
|---|---|---|
| 文書全体の構造検証 | 全体構造がなく、フィールドの逸脱に気づけない | JSON 配列 + Schema |
| N 番目のレコードへのランダムアクセス | 行長が可変で二分探索できない | SQLite / Parquet |
| 深いネストとレコード間参照 | 行が独立しており、参照の置き場所がない | 標準の JSON 文書 |
一言の経験則:レコードが時間とともに届く、または行単位で消費される(ログ、イベント、エクスポート、学習データ)なら JSONL。小さなデータを一度作り一度消費し、文書全体の意味論が必要なら JSON 配列。 ただし RS 区切りの JSON Text Sequences などの派生形式は別の話で、RFC 7464 の記事 を参照してください。
FAQ
JSONL と NDJSON に実質的な違いはありますか?
工学的にはありません。どちらも「1 行 1 完全 JSON 値、外側のラッパーなし、行間カンマなし」を意味し、片方に対応するツールはもう片方にも対応します。違いは呼称とエコシステムの慣習です。データエンジニアリングと ML データセットでは .jsonl が支配的で、ストリーミング HTTP API では .ndjson と application/x-ndjson が支配的です。外部システムと統合するときは、そのドキュメントが指定する拡張子とメディアタイプに従います。
.jsonl を JSON.parse で直接読めますか?
全体としては読めません。JSON.parse は正確に 1 つの JSON テキストを受け取りますが、JSONL は行に並べられた多数の独立した値なので、全体パースは必ず「Unexpected token」や「Extra data」で失敗します。行に分割し、空でない各行を個別にパースして、失敗時に行番号を報告してください。
JSONL の 1 行が壊れていたら、ファイル全体がダメですか?
いいえ。行単位の障害分離はこの形式の中核設計です。壊れた行は自分自身にしか影響せず、スキップして行番号を記録し、読み取りを続けられます。一括取り込みでは壊れた行を別ファイルに隔離し、正常レコードは通常どおり取り込み、壊れ行の比率に基づいて修正と再取り込みを判断します。
JSONL を Excel や CSV に変換するには?
まず配列に変換するか、行ごとに必要なフィールドを抽出してからテーブル変換に渡します。jq -c '.[]' data.jsonl で圧縮した結果をコンバータに渡すか、各行を json.loads で pandas に流し込みます。注意点として、ネストしたフィールドを展開すると列名がドット付きになり、配列フィールドはセル内でテキスト化せざるを得ず、長い ID 列は精度を守るために事前に文字列として宣言すべきです。