DevKitLab Logo DevKitLab
JSON / デバッグ / 整形

パースできない JSON を直す方法(残った混乱も片付ける)

パースエラーは、そもそも入力が厳密な JSON ではなかったという合図です。エラーの読み方、壊す 5 つの原因、そして読める形に整えるまでを解説します。

乱雑なログ行がインデントの整った JSON に変わり、パースエラーの位置が示されている

同僚が Slack に一行貼ってきます。「これ、なんで落ちるの?」。ある Python サービスのログから切り出してきた断片で、読めるようにきれいな JSON にしたいのだそうです。フォーマッターに貼り付けて整形を押すと、赤くなります。無効な JSON、と。

ここでつまずきやすい点があります。たいていの場合、ツールが正しくて入力のほうが間違っているのです。修復不能なほど壊れているという意味ではなく、そもそも厳密な JSON ではなかったという意味で。それは print された Python の辞書だったり、誰かがログに出した JavaScript のオブジェクトだったり、別のペイロードを文字列として中に詰め込んだデータだったりします。どれも一見 JSON に見えますが、どれもパースできません。この記事では、実際にぶつかる順番でこの混乱をほどいていきます。エラーを読む、なぜ JSON でないのかを突き止める、いちばん厄介なネストのケースに対処する、そして——ようやくパースできたら——読める形に整える。

直す対象のかたまり

これが同僚の貼り付けたもので、現実にかなり近いものです。

{
  users: [
    {'id': 1, 'name': "Ada", 'active': True, 'meta': '{"role":"admin","seen":null}'},
    {'id': 2, 'name': "Grace", 'active': False, 'meta': None},
  ]
}

目を細めれば、明らかにデータです。しかし最初のトークンで失敗し、しかもたった 5 行の中に、それぞれ独立した失敗理由が少なくとも 4 つ埋まっています。ひとつずつ片付けながら、これを記事全体を通す例として使います。

まずはエラーを読む——見た目より役に立つ

何かを直す前に、パーサーが実際に何と言ったかを読みましょう。JSON.parse は漠然と失敗するのではなく、特定の 1 文字で失敗し、メッセージがそれがどこかを教えてくれます。厄介なのは、どこで実行したかによってメッセージの見た目が変わることで、これが多くの人を混乱させます。

位置を数えやすいように、もっと小さな壊れた例——末尾カンマ——を取り上げます。

{"name": "Ada",}

古い V8 エンジン(Node 21 より前、古い Chrome)はこう報告します。

SyntaxError: Unexpected token } in JSON at position 15

position 15 は文字列の先頭からの文字オフセットで、ゼロから数え、スペースも引用符も含みます。15 まで数えると、ちょうどあの } に行き着きます——パーサーはカンマのあとに次のキーを期待し、代わりに閉じ括弧を受け取ったのです。この数字こそが要点で、漠然とした苦情ではなく位置であり、たいていはそれだけで余計な文字を見つけるのに十分です。

新しい V8(Node 21 以降、新しめの Chrome)はこのメッセージを書き換え、しかも——やや厄介なことに——その素朴なオフセットを落としました。

SyntaxError: Unexpected token '}', "{"name": "Ada",}" is not valid JSON

今度は数字ではなく、周辺テキストの断片が返ってきます。一方で Firefox は、三者の中でずっといちばん親切です。

SyntaxError: JSON.parse: expected property name or '}' at line 1 column 16 of the JSON data

行と列に加えて、何を期待していたかまで教えてくれます。エンジンは 3 つ、言い回しも 3 通り、しかし底にある事実は同じです。ある正確な一点に、そこにあってはいけないものがある、と。フォーマッターがエラー位置を示し、その行と列へ一足飛びに移動させてくれるとき、省いてくれるのはこの手順です——手で文字を数えるのをやめて、その一点を見に行くだけになります。あの大きなかたまりを JSON フォーマッターに貼り付ければ、最初の違反箇所で止まって指し示してくれます。エラーをクリックすれば、カーソルがそこに落ちます。

というわけで、このかたまりで最初に違反しているのは、いちばん上の usersu です。これがなぜへとつながります。

なぜ JSON でないのか:正しく見えて実は違う 5 つ

厳密な JSON は、あえて狭くて味気なくしてある形式です——だからこそ移植性が高い。「無効な JSON」の多くは、実は別の何かとして有効で、ただ波括弧を借りているだけです。次の 5 つが絶えず現れ、そして私たちのかたまりはそのうち 4 つを抱えています。

キーはダブルクォートが必要。 {users: [...]} は完全に正しい JavaScript のオブジェクトリテラルであり、JSON ではありません。JSON は "users" を要求します。コピーしてきた JS オブジェクトや、その console.log の出力がパースできない最大の理由がこれです——言語は引用符の省略を許しますが、交換フォーマットは許しません。

文字列はシングルではなくダブルクォート。 'name''admin' も失格です。JSON は " しか知りません。Python の print()、Ruby の inspect 出力、そして多くのログフォーマッターがシングルクォートを吐くので、そこからコピーしたものはここで即座につまずきます。

Python の真偽値と null は JSON のものではない。 このかたまりには TrueFalseNone があります——それらは Python のリテラルです。JSON は小文字で truefalsenull と綴ります。同じ罠が NaNInfinity にもあり、いくつかのライブラリはこれらを吐きますが、厳密な JSON はきっぱり禁じています。JSON には「数ではない」を書く合法な方法がありません。往復変換を当てにする前に覚えておきましょう。

末尾カンマは不可。 最後のユーザーの後ろのカンマ——None}, に続く ]——は、ほぼすべてのプログラミング言語が今では許すにもかかわらず、JSON では不正です。これがいちばん深く刺さります。エディターも linter も、私たちを「どこにでも末尾カンマを置く」よう訓練してきたからです。JSON はいまだにそれを拒む唯一の場所です。

コメントは不可。 私たちのかたまりにはありませんが、同じ仲間です。///* */ も JSON ではありません。コメント付きの tsconfig.json や VS Code の設定ファイルを編集しているなら、それは上位集合の JSONC です——厳密なパーサーはそのコメントで詰まります。

これらは手で直せますし、5 行なら手作業で十分です。もっと大きくなると面倒でミスも増えるので、良いフォーマッターには修復のパスがあります。まず厳密な JSON として試し、次第に緩い解釈——シングルクォート、Python リテラル、末尾カンマ——へと進み、何を仮定せざるを得なかったかを教えてくれます。修復は保証ではなく便宜です。ログの一行をさっと読むためのものであって、信頼できない入力にお墨付きを与えるためのものではありません。とはいえこのかたまりに対しては、当然のことをして、パースできる JSON を返してくれます。

いちばん厄介なやつ:JSON の中の JSON

上の 4 つの問題を直せば、このかたまりはパースできます——しかし Ada の meta フィールドを見てください。

"meta": "{\"role\":\"admin\",\"seen\":null}"

その値はオブジェクトではありません。たまたま JSON を含む文字列で、エスケープされて外側のドキュメントに詰め込まれたものです。一度気づき始めると、これはあちこちにあります。シリアライズされた JSON を保持するテキスト型のデータベースカラム、本当のペイロードを文字列としてネストする webhook、デコードすると中身がまた JSON でもう一度デコードが要る JWT、書き込む前にオブジェクトを文字列化したログフィールド。普通のフォーマッターは、その値を長い一本のエスケープ文字列として見せて、あとはあなたに片付けさせるだけです。

ここでの手は 2 つ。貼り付けたものぜんたいが JSON 文字列なら——全体が引用符で包まれ、そこかしこに \" があり、シリアライズされた値をコピーするとちょうどこうなります——一度アンエスケープして、構造化されたドキュメントに戻します。もしそれが、一部のフィールドに文字列化された JSON を抱えたドキュメントなら、たとえば私たちの meta のように、その場でネストされた文字列を展開して、"{\"role\":\"admin\"}" を、読めて折りたためる本物のネストオブジェクトに変えるのがよいでしょう。JSON フォーマッターには両方あり、なかでもネスト文字列の展開は、見つけて最もほっとする機能です——二重エンコードされたデータにつきものの「この値を別タブにコピー、貼り付け、アンエスケープ、また戻す」という往復のダンスを省いてくれます。

展開すると、Ada のレコードはようやくデータらしく読めます。

{
  "id": 1,
  "name": "Ada",
  "active": true,
  "meta": { "role": "admin", "seen": null }
}
修復とネスト文字列の展開の後の JSON フォーマッター。以前はエスケープ文字列だった meta フィールドが本物のネストオブジェクトになり、カーソル位置の JSON path がステータスバーに表示されている
修復が構文を直し、ネスト展開が文字列化された meta フィールドをオブジェクトに戻し、ステータスバーが JSON path を表示する。

パースできたら次は:圧縮、整形、それともキーの並べ替え

有効な JSON はスタートラインであってゴールではありません。次に何をするかは、そもそもなぜ読める形にしたかったかで決まります。

**整形(pretty-print)**が既定です——インデントして、それぞれを行に分けて、目で追いやすくする。2 スペースのインデントがよくある流儀で、多くのフォーマッターが出力する形です。レスポンスを読んだり、ペイロードをデバッグしたりするときに欲しいのはこれです。

**圧縮(minify)**は逆で、スペースと改行をすべて剥ぎ取って一行にします。JSON が何かの中に入るとき——リクエストボディ、環境変数、一行のログ、別の JSON ドキュメントのフィールド——に欲しい形です。圧縮された JSON を読む人はいません。それは回線のためのものであって、あなたのためのものではありません。

**キーの並べ替え(sort keys)**は、いちばん活用されていないものです。JSON オブジェクトには定まったキー順がないので、2 つのサービスが同じレコードを異なるフィールド順で吐くことがあります——そして 2 つのペイロードを比べようとした瞬間、その不安定な順序が本当の変更をノイズの下に埋めてしまいます。フィクスチャを保存する前、あるいは diff の前に、キーを再帰的に並べ替えると出力が安定し、次の比較では本当に変わったものだけが見えるようになります。(ひとつ注意。並べ替えは順序を書き換えるので、元の順序に意味がある場合——フィールドが意図的にグループ分けされた手調整の設定——は先に原本をコピーしておきましょう。)これは2 つの JSON ファイルの比較で扱ったノイズの問題に直結します。まず両側を正規化すれば、diff は真実を語ります。

どこで整形するか:ターミナル、エディター、コード、それともブラウザー

唯一の正解の場所はありませんし、あるふりをするのは不誠実です。手元にあるものを使いましょう。

ターミナル。 ファイルがディスク上にあるなら、jq はなかなか負けません。jq . file.json は整形し、jq -S . file.json は整形かつキーを並べ替え、jq -c . file.json は圧縮します。jq を入れていない? python -m json.tool file.json はほぼどのマシンにもあり、最初の構文エラーの行と列も報告するので、いざというときの検証にも使えます。ただし注意:jqpython も、まだ厳密な JSON でないものは拒みます。ですから最初のシングルクォートと True の混乱には役立ちません——エラーを報告するだけで、修復はしません。

エディター。 VS Code とその仲間は、内蔵フォーマッターで JSON ファイルを整形し(Shift+Alt+F、または保存時整形)、入力しながら構文エラーに下線を引きます。ファイルがすでに開いていて、すでに合法に近いときには最適です。どこかからコピーしてきて、まだファイルとして保存したくない断片には、それほど向きません。

コード。 プログラムの中でなら、JavaScript の JSON.stringify(value, null, 2) か Python の json.dumps(obj, indent=2, sort_keys=True) にすぎません。整形が一回きりの確認ではなく、スクリプトやビルド手順の一部として起きなければならないときに、これが正しい道具です。

ブラウザー。 貼って、きれいに見て、また取り出すという具体的な作業——そしてこれが日々「JSON を整形する」の大半の意味です——には、ブラウザーツールが摩擦の少なさで勝ちます。インストール不要、貼れば整形、ターミナルツールが拒む「ほぼ JSON」のケースも修復し、ネスト文字列の混乱もその場で処理します。パイプライン内の jq を置き換えるものではありません。ただ何かを読みたいだけのときの、あの十数秒の手間を置き換えるものです。

大きな JSON がなぜタブを固まらせるのか——そして本来そうならないはず

JSON を扱う人なら誰しも、この手でブラウザーのタブを一つ葬ったことがあります。数メガのログ出力をどこかのオンラインフォーマッターに貼ると、ページが固まり、ファンが回り、あきらめて閉じて jq に戻る。これは十分に頻繁に起きるので、少なからぬ人が、大きなものについてはブラウザーのフォーマッターを静かに見限っています。

たいていの元凶は素朴なレンダリングです。多くの Web フォーマッターは、整形済みのドキュメントぜんぶを一度にページの DOM に入れます——一行残らず、括弧も、構文ハイライトのスパンも——そしてキーを打つたびにその大部分をやり直します。数百行なら気づきません。数十万行になると、ブラウザーはどんなページにも許されない数の DOM ノードをレイアウトし描画しようとして、止まってしまいます。

解決策は、誰も見ていないものを描画するのをやめることです。JSON フォーマッターは、今ビューポートにある行だけを描画するエディター(CodeMirror)の上に作られています——スクロールすれば、いま現れた行を組み立て、離れた行は捨てます。ドキュメントは巨大でも DOM は小さいまま。DOM が抱えるのは、実際に見えている一画面分だけだからです。構文ハイライトも増分的で、編集は変わった部分だけを再解析し、ファイル全体を再パースはしません。より重い構造的な処理(たとえば全分岐を一度に折りたたむ)は時間予算のもとで走り、時間切れなら潔く引き下がって、タブを固めることはありません。さらに折りたたみの要約——折りたたまれた配列は 5 elements、折りたたまれたオブジェクトは 10 keys と表示——を足せば、すべてを折りたたんで気になる枝だけを開くことで巨大なペイロードを見て回れ、その間ずっと残りをレイアウトせずに済みます。

これで JSON.parse そのものがタダになるわけではありません。本当に巨大なファイルのパースは、最初にそれなりのコストがかかります。しかし「滑らか」と「固まる」を分けるのは、ほとんどの場合パースではありません。差はパースの後に DOM 側で起きることにあり、ビューポートだけを描画することが、それを滑らかに保つのです。

オンラインフォーマッターに貼っても安全か

立ち止まって考える価値があります。正直な答えは「何を貼るか次第」だからです。整形したい JSON の多くは、まさに機微なもの——本番の API レスポンス、デコード済みのトークン、内部設定、顧客データを含む webhook の body——です。もしフォーマッターがその処理のためにテキストをサーバーへ送るなら、あなたは第三者に写しを一つ渡したことになり、「ほんの一瞬だけ」と言っても、送ってしまった事実はもう取り消せません。

これを避ける方法は、プライバシーポリシーを信じることではなく、そもそもネットワークを必要としないツールを使うことです。パース、修復、整形、折りたたみ、ファイル読み込みがすべて JavaScript でブラウザー内で走るなら、貼り付けたテキストはあなたのマシンにとどまります——何もアップロードされないので、漏れるものもありません。それこそが探すべき性質であり、公開したくないものに対して、ローカルファーストのフォーマッターが安全な既定になる理由です。

合法な JSON でさえ生き残る 2 つの落とし穴

知っておく価値のあることが 2 つ。どちらもあらゆる検証をすり抜け、それでも噛みついてきます。

大きな整数は精度を失う。 JavaScript の数値は 64 ビット浮動小数点で、JSON.parse は数値をそこへ読み込みます。64 ビットのデータベース ID や Twitter 風のスノーフレークは、浮動小数点が正確に表せる範囲を超えることがあり、そのとき静かに丸められます。

JSON.parse('{"id": 9007199254740993}').id
// → 9007199254740992   ← もう元の数ではない

JSON は有効でした。その ID は今、静かに 1 だけずれていて、エラーは一つも出ていません。大きな整数 ID を JavaScript ベースの何かに通すなら、文字列として保ちましょう。

重複キー。 {"a": 1, "a": 2} は技術的には有効な JSON で、仕様はパーサーがどう扱うべきかを定めていません。実際には多くのパーサーが最後のものを残すので、これは {"a": 2} として読み戻され、最初の値は何も言わずに消えます。まれですが、生成されたドキュメントにフィールドが重複しているとき、片方が消えたように見える理由がこれです。

もう一度組み立て直す

私たちの Slack のかたまりは、シングルクォートと True/None、末尾カンマ、そして文字列の中に隠れたペイロードを持つ Python 辞書の出力として始まりました。抜け出す道は毎回同じ順序でした。エラーを読んでどこで壊れているかを見つけ、なぜそれが厳密な JSON でないのかを見抜き、本当のデータを隠していたネスト文字列を展開し、そのうえで初めて、次に何をするかに応じて整形・並べ替え・圧縮する。

整形はたいてい最初の一歩であって、仕事のすべてではありません。きれいになったら、2 つのバージョンを比べるのは構造的な JSON Diff の仕事、サンプルレスポンスを型付きのインターフェースに変えるのは JSON から TypeScript、深いペイロードからスクロールせずに一つのフィールドを取り出すのは JSONPath 評価ツールの役目です。仕事は違っても、始まりは同じ——本当にパースでき、ようやく読める JSON から。