本文へスキップ
jsonbeautifiers
日本語

実際にペイロードを壊すJSONの間違い10選

面白さではなく頻度の順に並べ、それぞれが実際に出すエラーメッセージを添えました。

このページの記述はすべて実測か出典付きです。そのどちらでもない場合は、そのことを明記しています。

「JSONが不正」というチケットのほとんどは、10種類のうちのどれかです。パーサーは位置を教え、ときには文字を教えますが、原因は決して教えません。以下、出現頻度のおおよその順に、あなたが目にしたはずのメッセージと直し方を並べます。

1. 末尾カンマ

圧倒的に多い間違いです。日常的に書くほかの形式がどれもこれを許しているからです。

{ "a": 1, "b": 2, }

V8が返すのは、カンマにもあなたのしたことにも触れないメッセージです。

Expected double-quoted property name in JSON at position 18 (line 1 column 19)

パーサーはカンマを読み進め、次のキーを期待し、}に突き当たりました。では同じことを配列でやってみます。

Unexpected token ']', "[1,2,]" is not valid JSON

まったく同じ間違いなのに文言がまるで違います。パーサー内で配列の経路が別の構文規則で失敗するからです。何が起きたのかを知ろうとしてエラー文字列を検索しても有用な情報が出てこないのは、この非対称性のせいです。

Pythonのほうが率直ですが、それも最近になってからです。3.13以降では、

Illegal trailing comma before end of object

3.12以前では同じ入力がExpecting property name enclosed in double quotes: line 1 column 19 (char 18)になります。同じ処理系、あなたのファイルの同じ不備、それがCIのピン留めしているバージョン次第で2通りの説明になるわけです。

直し方: カンマを削る。{ "a": 1, "b": 2 }

2. シングルクォート

json.dumps()ではなくprint()str()を通ったPythonのdictです。

{'ok': True}

これはJSONではありませんし、かつてそうだったこともありません。最初のクォートで失敗します。

Expected property name or '}' in JSON at position 1 (line 1 column 2)

Trueにも注目してください。最初の失敗の後ろに控えている、別個の2つ目の失敗です。JSONの真偽値は小文字です。

{"ok": true}

発生源で直しましょう。json.dumps(obj)、そして出力先がUTF-8のファイルやHTTPボディならjson.dumps(obj, ensure_ascii=False)とすれば、アクセント付き文字が\uXXXXエスケープにならず読める形のまま残ります。コンパクトにしたいならseparators=(",", ":")も。

3. 引用符のないキー

JavaScriptのオブジェクトリテラルをそのままJSONの欄に貼ったもの。

{ name: "ada", active: true }
Expected property name or '}' in JSON at position 2 (line 1 column 3)

JSONはすべてのキーがダブルクォートの文字列であることを要求します。シングルクォートでも、裸でも、数値でもいけません。{"name": "ada", "active": true}。前項と同種の貼り付けであり、直し方も同じです。コンソールのログからではなく、本物のシリアライザで値をランタイムから取り出してください。

4. エスケープされていない制御文字

文字列リテラルの中に本物の改行が入っている場合。

{"note": "line one
line two"}
Bad control character in string literal in JSON at position 18 (line 1 column 19)

PythonではInvalid control character at: line 1 column 19 (char 18)となります。いずれにせよパーサーは、エスケープしか許されない文字列の内側にU+0020未満の文字が現れた、と伝えているのです。

{"note": "line one\nline two"}

タブも同じ問題で、しかも見つけにくい。値に貼り込まれたタブはスペースにしか見えないからです。根本原因はほぼ常に、文字列連結で組み立てられたJSONです。改行を含むフィールドがそのまま差し込まれるのです。最後の節でこれをきちんと扱います。

5. Windowsのパス

{"path": "C:\Users\ada\config.json"}

\U\aも有効なエスケープではありません。Pythonは明快です。Invalid \escape: line 1 column 13 (char 12)。V8はBad escaped character in JSON at position 13 (line 1 column 14)と言い、バックスラッシュではなくUを指します。合法なエスケープは\" \\ \/ \b \f \n \r \t\uXXXXの9種類。それ以外はすべてエラーであり、これは設計として正しく、そして常に人を驚かせます。

{"path": "C:\\Users\\ada\\config.json"}

ほぼどのAPIでもWindows上でスラッシュは問題なく通りますし、バックスラッシュを二重にする手間もかかりません。埋め込みたいテキストの塊があって手作業は避けたいなら、エスケープツールが処理しますし、アンエスケープが逆方向を担当します。

6. 不可視文字

午後を丸ごと溶かすのがこれです。2つの変種があります。

ノーブレークスペース(U+00A0)。 ドキュメントページやチャットクライアント、PDFから断片をコピーすると、トークン間のスペースがスペースではないことがあります。RFC 8259がトークン間に許す空白文字はちょうど4つ、スペース、タブ、復帰、改行だけです。U+00A0はそこに入らないので構文エラーになり、しかも隣の文字と見た目が完全に同じです。

曲がった引用符。 WordやGoogleドキュメントは、入力中に直線的な引用符U+0022を組版用の対U+201CとU+201Dへ自動修正します。JSONはU+0022しか受け付けません。画面上では完璧に引用符で囲まれて見える文書に、文字列の区切り記号が1つも存在しない、ということが起きます。

どちらの変種も、コードポイントを名指しするメッセージは出しません。文字の位置によってはExpected double-quoted property name in JSON at position 8になったり、通常のスペースと見分けのつかない文字をそのまま印字し返すUnexpected token ' 'になったりします。文書をバリデーターに貼り付けてください。正確なオフセットで文字とそのコードポイントを名指しするので、これが最短経路です。JSON修復ツールはそれらを取り除き、何を除いたかを教えます。

7. コメント

{
  // ユーザーの表示名
  "name": "ada"
}

V8はExpected property name or '}' in JSON at position 4 (line 2 column 3)を返します。PythonはExpecting property name enclosed in double quotes: line 2 column 3 (char 4)で止まります。どちらもスラッシュを指し、どちらもコメントという語を口にしないので、文字列がひとつもない行についての引用符の問題のように読めてしまいます。

JSONにコメント構文はありません。人々がコメントでパース指示を運んでいたため、Crockfordが意図的に取り除きました。消費側を自分で握っているなら、JSONC(VS Codeが自身の設定に使っているもの)はコメントと末尾カンマを許し、JSON5はさらに多くを許します。握っていないなら、その散文はフィールドへ、あるいは説明文の本来の置き場所であるスキーマへ移しましょう。設定形式を選んでいる最中なら、詳しい議論に10分を割く価値があります。

8. NaNとInfinity

{"ratio": NaN}
Unexpected token 'N', "{"ratio": NaN}" is not valid JSON

厄介なのは、Pythonがこれを既定で出力することです。json.dumps({"ratio": float("nan")}){"ratio": NaN}を生成し、例外は投げません。CPythonのエンコーダーが意図的に寛容で、自身のデコーダーがその値を読み戻せるからです。Python以外の消費側はすべてこれを拒否します。

json.dumps(obj, allow_nan=False)   # 不正なJSONを送り出す代わりにValueErrorを投げる

今日のうちにシリアライズ層でこれを有効にしてください。本番に到達したNaNは、あなたが守り損ねた除算です。それを見つける場所は、顧客のパーサーの中よりエンコーダーのほうがましです。

9. キーの重複

{"id": 1, "id": 2}

エラーは一切出ません。RFC 8259はキーが一意であるべき(SHOULD)と述べるにとどまり、そうでない場合の挙動は未定義のままです。JavaScriptもPythonも最後のものを採るので、これは{"id": 2}としてパースされ、最初の値は跡形もなく消えます。最初のものを採るパーサーもあれば、例外を投げるものもあります。この一覧で唯一、何も言わずに進む項目であり、それゆえ最悪の項目です。ペイロードをバリデーターに通してください。黙って畳み込む代わりに重複を指摘します。

10. 数値

この枠は2つの失敗が分け合っています。

先頭のゼロ。 {"code": 007}は不正です。JSONの文法が許すのは単独の0か、1から9の数字に続けてさらに数字が並ぶ形だけで、それ以外はありません。先頭にゼロが付く郵便番号、国番号、部品番号は文字列です。{"code": "007"}

2^53-1を超える整数。 {"id": 12345678901234567890}は問題なくパースされ、別の数値になって返ってきます。JavaScriptがこれをIEEE 754のdoubleとして保持し、Number.MAX_SAFE_INTEGERが9007199254740991だからです。エラーも警告もなく、レコードだけが違う。大きなIDは文字列で送ってください。それ以外のあらゆる対処がなぜ回避策にすぎないのかは、長い版で扱っています。

構造的な直し方

この一覧の半分(2、3、4、5番)は同じ習慣から生まれます。シリアライザ以外のもの、たいていは文字列連結やコンソールのログでJSONを作ることです。

# どれも適切な入力を待っているだけのバグ
body = '{"note": "' + note + '", "path": "' + path + '"}'

noteに改行が入れば壊れます。pathにバックスラッシュが入れば壊れます。どちらかに引用符が入れば壊れますし、その入力がユーザー由来なら、それは書式の問題ではなくインジェクションです。

body = json.dumps({"note": note, "path": path}, allow_nan=False)

シリアライザはエスケープすべきものをエスケープし、引用符で囲むべきものを囲み、表現できないものを拒否します。これは好みの問題ではありません。JSONを手でテンプレート化するとは、RFC 8259第7節のエスケープ規則をすべての分岐で正しく再実装するということであり、誰もそれをやり切れません。

壊れた生成器ではなく壊れた文書を渡された場合は、JSON修復ツールが上記の修正を適用し、加えた変更をすべて一覧で表示します。出力を信用する前に、どこかで推測が入っていないかを自分で確かめられます。