JSON Toolkit

RFC 8259 から読む JSON の文法境界

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

初級約 11 分で読めます2026-10-30 公開2026-10-30 更新
RFC 8259 から読む JSON の文法境界のカバー画像

要点: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 ) ] "]"

重要な点は六つだけです。

  1. オブジェクトは波括弧を使い、メンバーはカンマで区切る。最後のメンバーの後のカンマは許されない。
  2. 配列は角括弧を使い、要素はカンマで区切る。末尾カンマも同様に許されない。
  3. 数値は先頭の + なし、先頭のゼロなし(01 は不正)、小数点のみの形なし(1. は不正)、NaN や Infinity もなし。
  4. 文字列は二重引用符を使う。単一引用符は不正。
  5. 真偽値と null は小文字のリテラル。True も NULL も不正。
  6. 空白文字 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 節は三つのことを述べています。

  1. 交換に使う JSON テキストは UTF-8 で符号化しなければならない。
  2. 実装はテキストの先頭に BOM(EF BB BF)を付けてはならない。
  3. パーサーは堅牢性のために 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 が拒否する」のはほぼ常に方言の問題です。変換するときは、まずコメント、単一引用符、末尾カンマを取り除き、その結果を標準パーサーに渡してください。