RFC 8259 から読む JSON の文法境界
要点:RFC 8259 の文法生成規則を平易な言葉に置き換えます。どの文字をそのまま書けるのか、どれをエスケープしなければならないのか、そして仕様が意図的に余地を残している箇所はどこか。

要点:JSON の構文エラーが出たら、文書全体を読み直すのではなく、最初の不正な文字を見つけて単一引用符・末尾カンマ・コメント・制御文字のどれかに分類しましょう。エンコーディング面では BOM なしの UTF-8 を確認し、2^53 - 1 を超える整数は文字列として扱います。
仕様書の本体は十数ページしかありませんが、その一つ一つの「MUST」「SHOULD」「MAY」の裏には、かつてチームを一晩中眠らせなかった出来事が隠れています。
1. 三つの文書の関係
JSON を学ぶときに混乱しやすいのは、RFC 4627、RFC 7159、RFC 8259 という三つの番号と ECMA-404 です。関係は実はとても単純です。
| 文書 | 日付 | 状態 | 主な違い |
|---|---|---|---|
| RFC 4627 | 2006 | 廃止 | トップレベルはオブジェクトか配列でなければならない |
| RFC 7159 | 2014 | 廃止 | トップレベルが任意の JSON 値に緩和 |
| RFC 8259 | 2017.12 | 現行標準 | UTF-8 を明示、相互運用性の指針を追加 |
| ECMA-404 | 2013、第 2 版 2017.12 | 文法仕様 | 文法のみを定義し、エンコーディングや転送は扱わない |
どれを読むべきか。 RFC 8259 だけで十分です。ECMA-404 は文法部分が等価で、違いは RFC 8259 が「UTF-8 で転送する」「実装者が注意すべき相互運用性の問題」まで定めている点にあります。
2. 六つの値の型
仕様の文法生成規則は、一つの図にまとめられます。
JSON-text = ws value ws
value = object / array / number / string / "true" / "false" / "null"
object = "{" [ member *( "," member ) ] "}"
member = string ws ":" ws value
array = "[" [ value *( "," value ) ] "]"
重要な点は六つだけです。
- オブジェクトは波括弧を使い、メンバーはカンマで区切る。最後のメンバーの後のカンマは許されない。
- 配列は角括弧を使い、要素はカンマで区切る。末尾カンマも同様に許されない。
- 数値は先頭の
+なし、先頭のゼロなし(01は不正)、小数点のみの形なし(1.は不正)、NaNやInfinityもなし。 - 文字列は二重引用符を使う。単一引用符は不正。
- 真偽値と null は小文字のリテラル。
TrueもNULLも不正。 - 空白文字
0x20(スペース)、0x09(タブ)、0x0A(改行)、0x0D(復帰)だけが構造要素の間に現れてよい。
次の断片は正しい JSON です。初めて見る人は誤りだと思うかもしれませんが、これはトップレベルの裸の文字列だからです。
"just a string"
RFC 7159 以降、トップレベルは確かに任意の値でかまいません。ただし API ではオブジェクトか配列のみを使うことをおすすめします。理由は第 6 節に書きます。
3. 文字列の中にそのまま書けるもの
ここが「自分の環境では動く」に最も騙されやすい部分です。仕様の文字列に関する規則は次のとおりです。
- そのまま現れてよい文字:
%x20-21、%x23-5B、%x5D-10FFFF。 - つまり、二重引用符
"(0x22)とバックスラッシュ\(0x5C)はエスケープしなければならない。 - 0x20 未満の制御文字はすべてエスケープが必要。改行、タブ、復帰も含む。
- 使えるエスケープシーケンスは
\"、\\、\/、\b、\f、\n、\r、\t、\uXXXXのみ。
{
"ok": "中国語のテキスト、絵文字 🎉、全角記号はそのまま書ける",
"escaped": "ここに改行\nが入り、タブ\tで終わる",
"control": "\u0007 はベル文字で、エスケープが必要",
"quote": "彼は言った:\"単一引用符は使わないでください\""
}
\/ はレガシーなエスケープです(</script> が早く閉じるのを防ぎます)。現代の場面ではほとんど必要ありませんが、パーサーは対応していなければなりません。
4. エンコーディング:UTF-8、BOM なし
RFC 8259 の 8.1 節は三つのことを述べています。
- 交換に使う JSON テキストは UTF-8 で符号化しなければならない。
- 実装はテキストの先頭に BOM(
EF BB BF)を付けてはならない。 - パーサーは堅牢性のために BOM を無視してもよい。
現実によくある二つの失敗例:
- Windows のメモ帳で「UTF-8(BOM 付き)」として保存すると、
JSON.parseにUnexpected tokenで拒否される。 - 中国語の JSON ファイルを GBK で保存し、UTF-8 としてのみデコードするパイプラインを通すと文字化けする。しかも構文はたまたま通ってしまうので、エラーは業務層まで先送りされる。
対処は単純です。ファイルを読むときに BOM を取り除き、明示的に UTF-8 としてデコードする。書き出すときは BOM を付けない。
/**
* 文字列先頭の UTF-8 BOM を取り除く。
* @param text 元のテキスト
* @returns BOM を取り除いたテキスト
*/
function stripBom(text: string): string {
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
}
ここでメディアタイプについても触れておきます。application/json はもはや charset パラメータを受け付けません。書いてもエラーにはなりませんが、時代遅れの慣行です。
5. 仕様が意図的に余地を残す三つの箇所
仕様が踏み込まない箇所こそ、実装差が生まれる場所です。
1. 数値の範囲と精度
RFC 8259 は「この仕様は数値の範囲や精度を制限しない」とだけ述べ、ほとんどの実装が IEEE 754 倍精度を使うことを注記しています。
つまり同じ 9223372036854775807 が JavaScript では 9223372036854776000 になり、Java の long では正確なままです。
結論:2^53 - 1 を超える整数や、多くの小数桁精度を必要とする金額は、常に文字列として扱い、スキーマには pattern を明示的に書くべきです。
2. 重複キー
仕様の文言は「オブジェクト内の名前は一意であるべき(SHOULD)」であり、一意でない場合の動作は述べていません。
JSON.parseは最後の一つを残す。- 最初の一つを残すパーサーもある。
- 単にエラーを投げるライブラリもある。
結論:重複キーを書かないこと。バリデータで特定の振る舞いに依存しないこと。ビルド時に「重複キー検出」のステップを追加する方が、事後に調査するよりはるかに安上がりです。
3. 深さと長さの制限
仕様はネストの深さに制限を設けていませんが、どの実装にも限界があります。V8 は数万レベルのネストで RangeError: Maximum call stack size exceeded を投げます。
サーバーは自前の制限を設けなければなりません。これは仕様の要件ではなく、セキュリティの要件です。
6. 反例で理解を確かめる
以下の各行はすべて不正な JSON です。パーサーに渡して、エラーメッセージが役に立つか見てみましょう。
{ 'name': 'json' } // 単一引用符
{ name: "json" } // キー名が引用符なし
{"a": 1,} // 末尾カンマ
[1, 2, 3,] // 配列の末尾カンマ
{"a": 01} // 先頭のゼロ
{"a": +1} // 先頭のプラス記号
{"a": .5} // 整数部の欠落
{"a": NaN} // 非標準リテラル
{"a": undefined} // JSON 値ではない
{"a": 1} // comment // コメント
興味深い境界例:{"a": 1} の後にコメントを付けたものは、JSON5 では正しく、JSONC でも正しく、標準 JSON では不正です。
ですから「設定ファイルは動くのに API が拒否する」というのは、ほぼ常に方言の問題です。
7. 本番投入チェックリスト
- [ ] 外部に送るすべての JSON は
charsetなしのapplication/jsonを使う。 - [ ] ファイル読み書きのパイプラインは BOM を取り除き、明示的に UTF-8 を使う。
- [ ]
2^53を超える整数は文字列で扱い、スキーマにも注記する。 - [ ] 重複キーはビルド時に検出する。
- [ ] サーバーはリクエストボディのサイズとネストの深さを制限する。
- [ ] 公開 API のトップレベル値はオブジェクトか配列で、裸のスカラーにしない。
FAQ
あるテキストが正しい JSON かどうかを素早く見分けるには?
テキストをパーサーまたはオンラインパースツールに渡し、最初の不正な文字の位置を読み、RFC 8259 の 6 つの値の型と空白規則に照らして確認します。トップレベルは任意の JSON 値でかまいませんが、構造要素の間にはスペース、タブ、改行、復帰しか許されません。単一引用符、引用符なしのキー、末尾カンマ、コメントがあれば、それは標準 JSON ではありません。
なぜ JSON は UTF-8 を使い、BOM を付けてはならないのですか?
RFC 8259 の 8.1 節は、交換に使う JSON テキストを UTF-8 で符号化することを求め、実装が先頭に BOM を付けてはならないと定め、堅牢性のためにパーサーが BOM を無視してもよいとしています。BOM 付きのファイルは、目には何もおかしく見えなくても JSON.parse に Unexpected token で拒否されます。ですから読み込むときに BOM を取り除き、明示的に UTF-8 としてデコードしてください。
これらの文法上の制限を回避するために JSON5 や JSONC を使うべきですか?
はい。ただし設定ファイルのようなローカルな読み取りに限ります。システム間で交換するデータは標準 JSON のままにすべきです。反例が示すとおり、{"a": 1} の後にコメントを付けたものは JSON5 と JSONC では正しく、標準 JSON では不正です。だからこそ「設定ファイルは動くのに API が拒否する」のはほぼ常に方言の問題です。変換するときは、まずコメント、単一引用符、末尾カンマを取り除き、その結果を標準パーサーに渡してください。