本文へスキップ
jsonbeautifiers
日本語

NDJSONをJSONに変換

改行区切りのレコードを1つの配列にまとめ、失敗した行を報告します。

NDJSON
JSON配列

貼り付けたものがブラウザの外に出ることはありません。 connect-src の許可リストにより、これは約束ではなくブラウザによる保証になっています。 自分で確かめる

改行区切りのJSONを、ひとつの配列にまとめます。各行は独立してパースされ、失敗した行はファイル全体を巻き込まずに行番号とともに報告されます。

NDJSONは、ログのパイプライン、BigQueryのエクスポート、Elasticsearchのbulkファイル、ストリーミングAPIから出てくる形式です。そしてそれを1つのドキュメントとしてパースしたときに、「Extra data」や「unexpected non-whitespace character」というエラーを起こす当のものでもあります。

NDJSONとは何か

1行に1つの完結したJSON値を置き、改行で区切ります。レコード間のカンマも、全体を包む配列もありません。空行は無視されます。慣例的な拡張子は.ndjsonと.jsonlです。

JSON LinesとNDJSONは、実質的に同じ形式です。2つの小さな仕様が、重要な点ではすべて一致しています。ツールによってどちらの名前を使うかは異なりますが、どちらのつもりで書かれたファイルでも、双方で正しく読めます。

なぜこの形式があるのか

実利は3つあり、いずれもレコードが互いに独立していることから生まれます。

ストリーミングできる
受け取る側は1レコードずつ処理し、ファイル全体を抱え込みません。50GBのエクスポートでも問題ありませんが、50GBのJSON配列は問題です。
追記できる
レコードの追加は、ファイル末尾への1回の書き込みで済みます。JSON配列への追加は閉じ括弧の書き直しを伴い、それはもはや追記ではありません。
破損に強い
壊れた行1つで失うのはレコード1件です。JSON配列の中で1バイト壊れれば、失うのはファイルそのものです。

どちらの方向に変換するか

単一のドキュメントを期待する場所へ渡すなら配列に。ブラウザ、リクエストのボディ、設定ファイルなどです。パイプライン、ログ、追記専用のファイル、ストリーミングする何かへ渡すならNDJSONに。上でどちらの方向も選べます。

How to do this in code

コードでNDJSONを読み書きする方法。

py Python

リスト内包表記はすべてをメモリに載せます。ストリーム処理したい場合は、ファイルを直接イテレートしてください。

import json

# Read
with open('events.ndjson') as f:
    records = [json.loads(line) for line in f if line.strip()]

# Write
with open('events.ndjson', 'w') as f:
    for r in records:
        f.write(json.dumps(r) + '\n')

# pandas knows the format
import pandas as pd
df = pd.read_json('events.ndjson', lines=True)
sh jq

-sはすべての入力を1つの配列にまとめ、-cは1行に1つのコンパクトな値を書き出します。この2つのフラグだけで変換は完了します。

# NDJSON to an array
jq -s . events.ndjson > events.json

# An array to NDJSON
jq -c '.[]' events.json > events.ndjson

# Filter a huge NDJSON file without loading it all
jq -c 'select(.level == "error")' events.ndjson
js Node

crlfDelay: Infinityを指定すると、readlineがCRLFを1つの改行として扱います。Windowsで書かれたファイルでは重要です。

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

const rl = createInterface({
  input: createReadStream('events.ndjson'),
  crlfDelay: Infinity,
});

for await (const line of rl) {
  if (!line.trim()) continue;
  const record = JSON.parse(line);
  // one record at a time, constant memory
}

よくある質問

NDJSONとJSON Linesは同じものですか?
実用上はすべて同じです。2つの小さな仕様が、重要な点では一致しています。1行に1つのJSON値、UTF-8、改行区切りです。.jsonlと.ndjsonという拡張子も互換的に使われています。
1レコードを複数行にまたがらせられますか?
できません。それがこの形式の要点です。改行がレコードの区切りなので、各レコードはちょうど1行に収まらなければなりません。書き出す前に各レコードを圧縮してください。
NDJSONファイルがJSONとしてパースできないのはなぜですか?
それが1つのJSONドキュメントではなく、多数のドキュメントだからです。JavaScriptは「Unexpected non-whitespace character after JSON」、Pythonは「Extra data」と報告します。どちらも、パーサーが1つの値を読み終えたあとに、さらに別の値を見つけたという意味です。