JSON Toolkit

同じ JSON なのに 4 つのツールが 4 通りの結果を出す:フォーマッタの選び方

要点:整形は無損ではありません。先頭ゼロ・BOM・20 桁整数を含む同じ JSON を Python、Node、jq、json.tool に通すと、エラーになるものもあれば黙って値を書き換えるものもあります。10 件の自走サンプルで 4 経路を比較し、選定表と貼り付け可能な厳密検証スクリプトを示します。

Intermediate約 6 分で読めます2026-11-03 公開2026-11-03 更新
同じ JSON に対して 4 つの解析経路が異なる結果を返す図

結論:フォーマッタの決定的な差はインデントの幅ではなく、同じ入力がエラーになるか、あるいは黙ってデータを書き換えるかです。機能一覧を読むより、境界値サンプルを 1 本走らせる方がはるかに確実です。

「整形は再排版にすぎない」と思われがちですが、実際には各経路が独自のパーサでテキストを解釈し直しています。パーサが寛容だと、出るはずのエラーが静かな値の書き換えに変わり、画面には「整形成功」の緑が灯ったままになります。

1. 現象:同じ入力、4 経路、4 つの結果

検証環境は Python 3.11.1、Node v22.13.1、jq 1.7。手作業で作った短い JSON 10 件を標準入力から送り、ディスクに落とさず設定も変えませんでした。経路は json.loads、JSON.parse、jq -c .、python -m json.tool の 4 つです。表中 ERR は例外で中断、SILENT はエラーなしだが出力が入力と一致しないことを示します。

サンプル python json.loads node JSON.parse jq -c . python -m json.tool
S1 {"a":1,"b":[1,2]} OK OK OK OK
S2 {"a":1,} ERR ERR ERR ERR
S3 {'a':1} ERR ERR ERR ERR
S4 {"a":01} ERR char 6 ERR position 6 SILENT {"a":1} ERR
S5 {"a":1 /*x*/} ERR char 7 ERR ERR ERR
S6 \ufeff{"a":1} ERR utf-8-sig ERR position 0 SILENT {"a":1} ERR
S7 {"id":9007199254740993} OK 忠実 SILENT 9007199254740992 OK 忠実 OK
S8 {"a":1,"a":2} SILENT {"a":2} SILENT {"a":2} SILENT {"a":2} OK
S9 {"a":1 ERR char 6 ERR ERR EOF ERR
S10 <html>502</html> ERR char 0 ERR Unexpected token '<' ERR ERR

10 件中 4 件(S4・S6・S7・S8)が「エラーなしだが値が変わった」でした。緑の成功表示だけを信じると、4 割の確率で元のデータとは別のものを手にしています。しかもこれらは構文エラー一覧に載らず、ファイルは開ける・コピーできる・書き出せます。誤りは構文ではなく中身にあります。

2. 原因:構文層と意味層は別物

RFC 8259 の 1.2 節は ECMA-404 が許す実践を相互運用性の観点から避けるよう助言し、4 節はオブジェクト名を SHOULD で一意としつつ重複キーの扱いを規定せず、6 節は数字について相互運用上の助言のみを与えます。ECMA-404 はさらに「数字の意味論には関知しない」と明言し、表現だけを定めます。つまり仕様は広い境界領域を実装者に委ねています。

そのため「整形」は二層に分かれます。構文層がテキストの妥当性を判定し、意味層がそのテキストをどんな値にするかを決める。前者には標準回答があり、後者にはありません。jq が 01 を 1 と読み、先頭 BOM を飛ばすのは意味層の選択、Node が 20 桁整数を IEEE 754 倍精度に押し込むのは実行時の選択、4 経路が {"a":1,"a":2} の後勝ちに揃うのは主要パーサの共通慣習です。どれも「解析できた」には反しませんが、すべてデータを変えています。

JSON パースエラーの直し方:Unexpected token を順に切り分ける は構文層を扱い、エラー原文を座標に翻訳します。本稿はその手前、ツールがそもそもエラーを出さなければ座標すら得られない、という段階を扱います。

3. 静かな書き換え 4 種を個別に見る

4 件を抜き出すと、変更は肉眼で見落とすほど小さいものです。

{"a": 01}                          // 先頭ゼロ:jq は {"a":1} を出力
\ufeff{"a": 1}                     // BOM:jq は先頭 3 バイトを飛ばす
{"id": 9007199254740993}           // 20 桁整数:JSON.parse は ...992 を出力
{"a": 1, "a": 2}                   // 重複キー:4 経路とも {"a":2} だけ残す

先頭ゼロと BOM は「入力が本来不正だが、ツールが寛容だった」型です。01 は標準 JSON では違法(RFC 8259 6 節)で、BOM も JSON テキスト先頭に置けません。Python と Node は拒否し、jq は続行します。ログ洗浄には便利ですが、契約検証では致命的です。上流は「不正データを送った、直す必要がある」と考え、下流は「普通に見える」結果を既に持っています。

20 桁整数は「入力は完全に合法だが、実行時が収め切れない」型です。9007199254740993 は合法な JSON 数値ですが 2^53-1 を超え、JSON.parse は 9007199254740992 しか表現できません。Python の int と jq は原値を保ちます。注文番号や Snowflake ID なら、末桁が 1 つ違うだけで照合が合わなくなります。

重複キーは「仕様が定めず、実装が各自選ぶ」型です。4 経路とも警告なしで後勝ち。RFC 8259 は一意性を SHOULD で述べるだけなのでバグではありませんが、設定の連結やパッチのマージで最も追いにくい類の問題です。

4. エラー報告:座標を出すもの、一行だけのもの

失敗した 6 件でも情報量は大きく違います。Python の JSONDecodeError は msg・lineno・colno・pos を同時に返し、Node v22 は position N (line 1 column M) と該当箇所の原文断片を添え、python -m json.tool は Python の情報を再利用します。jq は parse error: ... at line N の一行だけで、文字オフセットを出しません。

1 行に圧縮された JSON では行番号・桁番号はほぼ無意味です。必要なのは文字オフセットです。次のスクリプトは検証を「エラー+オフセット+前後文」に統一します。

import json, sys

def strict_check(text: str) -> None:
    try:
        json.loads(text)
        print("OK")
    except json.JSONDecodeError as e:
        print(f"ERR {e.msg} | char {e.pos} | line {e.lineno} col {e.colno}")
        lo, hi = max(0, e.pos - 30), min(len(text), e.pos + 30)
        print("...", text[lo:hi].replace("\n", "\\n"), "...")
        sys.exit(1)

strict_check(open(sys.argv[1], encoding="utf-8").read())

jq . より厳格(先頭ゼロと BOM を拒否)で、e.pos が得られます。line 1 column 23611 をバイト位置に換算する方法や、中文文書で文字オフセットとバイトオフセットが 3 倍違う理由は ミニファイされた JSON のデバッグ:1 行目 23611 列目から壊れたバイト位置へ に譲り、ここでは繰り返しません。

5. 直し方:整形を「厳格検証」+「美化」に分ける

結論は単純です。1 つの「整形」ボタンに検証と美化を兼ねさせないこと。まず厳格パーサで妥当性を判定し、それから排版方法を決めます。最初の一步は 1 行で切り出せます。

python3 -m json.tool input.json > /dev/null && echo "構文 OK" || echo "構文不正"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json

1 行目は「解析できるか」だけに答え、2 行目で「期待した構造か」を確認します。大きな整数があるなら JSON.parse ではなく Python か jq で読み、BOM の可能性があるなら encoding='utf-8-sig' を明示し、キー重複の可能性があるなら解析前に別途走査します(4 経路とも警告しないため)。これらを CI に入れるのは エディタ、コマンドライン、自動化ツールチェーン の流路そのものに、データ忠実性のアサーションを 1 層足した形です。

6. 選び方:4 経路の比較表

経路 強み リスク 向いている場面
python json.loads / json.tool 厳格、行列と文字オフセットを提示、整数の精度が落ちない 先頭ゼロと BOM を拒否、エンコーディング指定が必要 契約検証、CI、ログ調査
node JSON.parse ブラウザ標準、エラーに原文断片が付く 大きな整数を静かに丸める、BOM でエラー フロントのデバッグ、ブラウザ内ツール
jq -c . パイプ向き、最速で結果が出る 先頭ゼロと BOM が静かに通る、文字オフセットなし 運用パイプ、フィールド抽出
ブラウザツール /parse・/editor ツリー表示、キー整列、パスコピー、導入不要 挙動はページのパーサ次第、最初に 1 回検証が必要 目視確認、臨時トラブルシュート

決め手は「機能が多いか」ではなく「自分のデータについて嘘をつかないか」です。注文 ID・金額・デバイス ID を含むなら Python か忠実性を明記したツールを、構造を見るだけのデバッグならブラウザが最も手軽です。本サイトの /parse はブラウザ内で構文検証と整形を行うため「まず形を見る」に向きますが、大きな整数は上記スクリプトで再確認してください。

7. プライバシーは境界の 1 つにすぎない

「データがアップロードされるか」はオンラインツールで最も聞かれる質問ですが、「データが書き換えられるか」とは独立しています。ローカルでオフライン動作するツールでも 20 桁整数は丸められますし、厳格にエラーを出すオンラインツールが原文をサーバへ POST することもあります。2 つは別々に検証すべきで、互いに代用できません。プライバシー FAQ の 2 分で終わる 3 つの確認に対し、本稿が補うのは「ツールが静かに値を変えないか」を下のサンプルで確かめる半分です。

8. コピーできる自己点検リスト

S1〜S10 を 1 行ずつ cases.txt に保存し、候補ツールに貼り付けて 3 点を確認します。失敗すべきサンプルが実際に失敗するか、失敗時に具体的な位置が出るか、成功したサンプルの出力が入力と 1 文字も違わないか。特に S4・S6・S7・S8 を重点的に見て、SILENT が 1 行でもあればその経路は単独でデータ検証に使えません。

さらに 3 つの工学的制約を足します。厳格検証を pre-commit フックに入れ、本番で整数の不一致に気づくのを防ぐ。多言語環境ではファイルを常に BOM なし UTF-8 で書く。第三者由来の JSON は生レスポンスを保持し、整形結果は閲覧専用にして書き戻しに使わないこと。

よくある質問

整形が通れば JSON は問題なしと言えますか?

いいえ。S4・S6・S7・S8 はすべて整形に「成功」しましたが、先頭ゼロ・BOM・20 桁整数・重複キーが書き換えまたは除去されています。整形は「あるパーサが受け付けた」ことしか証明せず、値が送信値と等しいことは証明しません。重要フィールドは値ごとにアサーションを置いてください。

なぜ jq は Python が拒否する先頭ゼロや BOM を受け付けるのですか?

jq は独自の字句解析器を持ち、01 のような数字に寛容で、先頭 BOM も読み飛ばします。Python と Node は RFC 8259 に厳格に従います。バージョンのバグではなく、寛容パーサと厳格パーサの取舍です。洗浄では前者が省力、契約検証では後者が必須です。

大きな整数が 9007199254740992 になるのを防ぐには?

JavaScript の Number は IEEE 754 倍精度で、安全な整数の上限は 2^53-1。20 桁 ID を JSON.parse で読めば必ず丸まります。こうした ID は文字列で保存するか、Python の int、jq、BigInt 対応パーサを使ってください。フロントで数値が必要なら、解析前にそのフィールドを文字列に置き換えます。

オンラインツールとローカル CLI、どちらを使うべきですか?

データの機微度と用途で分けます。構造確認やフィールド探索ならブラウザが最速、パイプラインや忠実性のアサーションなら python -m json.tool と厳格チェックです。どちらも先に S1〜S10 を走らせ、エラーと出力が想定通りか確かめてから、どの種類のデータを入れるか決めてください。