RFC 8259: JSON
JSONを定義するRFC 8259をSTD 90として精読し、数値精度・重複キー・UTF-8など「仕様があえて決めていない」せいで本番バグになる箇所を、MUST/SHOULD/MAYの区別ごと押さえられる。
- RFC 8259はJSONの現行仕様でSTD 90。4627→7159→8259と改訂され、7159以降は数値や文字列単体も正しいトップレベル値になった。仕様本文は16ページである。
- 数値文法は任意精度だが、相互運用はIEEE 754倍精度を想定し、2^53−1超の整数はずれうる。大きなIDは文字列で運ぶ。NaN・Infinity・先頭ゼロ・先頭の+は文法外。
- 名前の一意性はSHOULDに留まり、重複時は先勝ち・後勝ち・エラーと実装差が出る。コメント・日付型・末尾カンマもない。制約を強めた互換プロファイルがI-JSON(RFC 7493)。
このRFCが決めたこと
横にスクロール
RFC 8259(2017年12月、Tim Bray編)は、JSONを定義する現行の文書だ。RFC 4627(2006年7月、Douglas Crockford)で始まりRFC 7159(2014年3月)を経た系譜の到達点で、STD 90すなわちインターネット標準に格上げされている。決めていることは驚くほど少ない。値は6種類(オブジェクト、配列、数値、文字列、true/false/null)、構造文字はわずか6個、そして本文はわずか16ページだ。
短さは手抜きではなく設計思想だ。JSONは「どう書くか」の文法をきっちり固定する代わりに、「その値をどう解釈し、どんな型に載せるか」の大半をアプリケーションと言語処理系に委ねている。実務のバグは、この委ねられた空白地帯にほぼ集中している。数値の精度、重複キーの扱い、日付の表現——どれもJSON自身は答えを持たず、実装ごとに答えが違う。
16ページのうち、値の文法を定義する部分は数ページに過ぎない。残りの多くは「これは相互運用性を壊しうる」という注意書きだ。JSONを正しく使うとは、文法を覚えることではなく、この注意書きが指す落とし穴を避けること。この記事はその落とし穴を仕様の言葉で一つずつ潰していく。
要点の精読
4627から8259への系譜とECMA-404
JSONの仕様は3世代を経ている。初版のRFC 4627は情報提供(Informational)扱いで、application/json というメディアタイプを登録した。RFC 7159が標準化トラックに乗せ、RFC 8259がインターネット標準として確定させた。改訂で最も影響が大きかったのは、7159での「JSONテキストの定義変更」だ。
4627ではトップレベルがオブジェクトか配列でなければならなかった。7159はこの制約を外し、JSONテキストを「直列化された任意の値」と再定義した。結果として、次はすべて単独で正しいJSONテキストになる。
42 ← 7159以降は正しいJSONテキスト(4627では不可)
"hi" ← 同上
true ← 同上
{"n": 42} ← 4627でもOK(トップレベルがオブジェクト)
もう一つの特徴が二重標準だ。同じ文法をEcma International がECMA-404として別途定義しており、その第2版もRFC 8259と同じ2017年12月に出た。8259はECMA-404を規範的(normative)参照として挙げつつ、「ECMA-404は本仕様が相互運用性のために避けるよう勧める慣行のいくつかを許している」と明記する。文法は同一だが、相互運用上の助言はIETF側が上乗せしている、という関係だ。両者に差異が見つかればECMAとIETFが協調して両文書を更新する、と取り決められている。
| 仕様 | 発行 | 位置づけ | この版の要点 |
|---|---|---|---|
| RFC 4627 | 2006年7月 | Informational | JSONを最初に記述し application/json を登録。トップレベルはオブジェクトか配列のみ |
| RFC 7159 | 2014年3月 | Proposed Standard | 4627を廃止。トップレベルを任意の値に拡張。エンコーディング節を整理 |
| RFC 8259 | 2017年12月 | Internet Standard (STD 90) | 7159を廃止。UTF-8を義務化。ECMA-404を規範参照に。既知の誤りを修正 |
| ECMA-404 | 2013年 / 第2版2017年12月 | Ecma標準 | 同じ文法を別建てで定義。8259とは文法一致・助言は8259が上乗せ |
UTF-8の義務化とBOMの扱い
エンコーディングは7159から8259への実質的な変更点の一つだ。7159は「JSONテキストはUTF-8、UTF-16、UTF-32のいずれかで符号化されるものとする(SHALL)」としていた。8259はこれを絞り込み、閉じたエコシステムの外でやり取りするJSONテキストはUTF-8で符号化しなければならない(MUST)と定めた。異なるシステム間で交換するなら、事実上UTF-8一択だ。
BOM(バイトオーダーマーク、U+FEFF)の扱いも明確になっている。生成側はネットワーク送信するJSONテキストの先頭にBOMを付けてはならず(MUST NOT)、解析側は先頭のBOMをエラー扱いせず無視してよい(MAY)。この規範語の強度差を意識すると、実装が守るべき境界が正確につかめる。つまり相手がBOMを送ってこない保証はない一方、自分が送る実装は絶対に付けてはならない。堅牢なパーサは受信時に先頭のBOMを黙って読み飛ばすのが無難だ。なおapplication/json メディアタイプには charset パラメータが定義されていない。文字コードはUTF-8前提で運ぶ、という設計がここにも表れている。
数値は文法と相互運用性がずれている
数値はJSONで最も現実のバグを生む場所だ。文法上、数値は十進で表され、任意の桁数を書ける。整数部・小数部・指数部からなり、次のルールがある。
正しい: 0 -3 3.14 1E400 1e-7 1.0
誤り: +3 03 .5 5. 0x1F NaN Infinity
先頭ゼロは禁止(03 は不可)、数値全体の符号として付けられるのは- だけで先頭の+ は許されない(指数部の1E+10 の+ は別)、そしてNaN やInfinity は文法に存在しない。この繊細な文法を手書きの正規表現で検証しようとすると、指数部や境界を取りこぼしやすい(正規表現パターン集で扱ったような落とし穴だ)。
問題はその先にある。文法は任意精度を許すのに、仕様の相互運用性に関する助言はIEEE 754倍精度(binary64)を前提に書かれている。仕様は、整数が[-(2^53)+1, (2^53)-1] すなわち-9007199254740991から9007199254740991の範囲にあれば、実装同士が値をぴったり一致させられる、と述べる。裏を返せば、この範囲を超える整数は受け取り側で丸められ、値が静かにずれうる。
JavaScriptの Number は倍精度浮動小数点数一本なので、安全に表せる整数は 2^53 - 1 までだ。{"id": 12345678901234567} のような64ビットIDをそのまま数値で送ると、JSON.parse の時点で下位の桁が失われる。だからTwitter/XのIDやSnowflake IDは、はじめから文字列 {"id": "12345678901234567"} として運ぶ。桁あふれしうるIDや金額は文字列にする、が実務の鉄則だ。
さらに1.0 と1 は文法上は区別されるが(片方は小数部を持つ)、仕様はどちらが整数でどちらが浮動小数点数かという意味を割り当てていない。この空白の埋め方は言語処理系任せで、JavaScriptはすべて倍精度、Pythonはint とfloat を使い分け、Goのencoding/json は既定でfloat64 に載せる。JSONの数値を言語の型へどう写すかは処理系ごとに違う、という前提を忘れると、往復(parse して再度 stringify)で値が変わる事故につながる(言語ごとの型の扱いはプログラミング側の話題だ)。
重複キーと順序
オブジェクトについて仕様が定める規範は、驚くほど緩い。曰く「オブジェクト内の名前は一意であるべき(SHOULD)」。MUSTではなくSHOULDなので、重複した名前を持つJSONは文法上は不正ではない。そして重複したときの挙動を、仕様は明確に「予測不能」と書く。多くの実装は最後の組だけを採り、別の実装はエラーにするか解析に失敗し、さらに別の実装はすべての重複を報告する、と例示される。
{"role": "user", "role": "admin"}
このJSONを、あるコンポーネントはuser と読み、別のコンポーネントはadmin と読む——それが仕様上あり得る。ここに現実のセキュリティ問題が潜む。認証を担う前段と権限判定を行う後段が別々のパーサを使い、重複キーの勝者について意見が食い違うと、検査をすり抜ける。これがパーサ差異(parser-differential)攻撃で、SHOULDをMUSTのように信頼した設計が生む典型的な穴だ。
順序についての規定も押さえておきたい。オブジェクトのメンバ順序は有意ではなく、パーサが呼び出し側に順序を見せるかどうかは実装によって異なる、と仕様は述べる。一方で配列の要素順序は有意だ。この非対称性が実務に効く。
| 構造 | 順序の意味 | 実務上の含意 |
|---|---|---|
| オブジェクト | 有意でない(メンバの並びに依存してはならない) | キー順に依存した比較・署名検証は壊れうる。並べ替えても同じ意味 |
| 配列 | 有意(要素の並びが意味を持つ) | 並べ替えは別のデータ。順序を保って読み書きする |
JSONに署名やハッシュをかけて完全性を検証する設計では、オブジェクトのメンバ順序が保存される保証がないことが罠になる。生成側と検証側で並びが変われば、同じ意味のJSONが別のバイト列になり検証が落ちる。JSON文字列そのものではなく、正規化(キーのソートなど)した表現に対して署名する必要がある。
JSONが決めていないこと(コメント・日付・I-JSON)
JSONの「無いもの」を把握しておくと、多くの設計判断が楽になる。仕様に存在しないのは主に次だ。
- コメント — 構文に無い。設定ファイルで欲しくなるが、JSONそのものには書けない(JSON5やJSONCといった非標準の拡張が別に埋める)。
- 日付・時刻型 — 型が無いため、慣習としてISO 8601(RFC 3339)形式の文字列
"2017-12-01T00:00:00Z"で表す。 - 整数と小数の区別 — 前述の通り数値は一種類で、
intとfloatを分けない。 - 末尾カンマ — 配列やオブジェクトの最後の要素の後にカンマを置けない。
- URIやバイナリの型 — 無い。URIは文字列にそのまま入れ(RFC 3986: URIの表現をそのまま値にする)、バイナリはbase64などの文字列にして運ぶ。
文字列にも落とし穴がある。文法上は"\uDEAD" のような対になっていないサロゲート(lone surrogate)を書けてしまうが、これを受け取ったソフトウェアの挙動は予測不能で、文字列長が食い違ったり実行時例外で落ちたりしうる、と仕様は警告する。書けることと相互運用できることは別だ。
これらの緩さを実務向けに締め直したのがI-JSON(Internet JSON、RFC 7493、2015年3月、Tim Bray)だ。UTF-8を必須とし、オブジェクトに重複した名前を認めず、サロゲートや非文字を排し、数値をIEEE 754倍精度の範囲に収めるよう求める。さらに時刻はRFC 3339、バイナリはbase64urlという慣習まで示す。8259が「あるべき(SHOULD)」で濁した箇所を「しなければならない(MUST)」に引き上げたプロファイル、と捉えるとよい。新しくAPIを設計するなら、8259の緩さに甘えるより最初からI-JSON相当の制約を課すほうが安全だ。
つまずきやすい点
第一に、64ビット整数を数値のまま送らないこと。前述の通り安全なのは2^53 - 1 までで、それを超えるIDや金額は文字列で運ぶのが定石だ。第二に、重複キーを「最後が勝つ」と決めつけないこと。それは一部の実装の挙動に過ぎず、仕様上は予測不能で、境界をまたぐと差異攻撃の入口になる。第三に、オブジェクトのキー順に依存しないこと。順序が有意なのは配列だけで、キー順を前提にした比較・署名・キャッシュキー生成は壊れうる。
第四に、規範語を読み分けること。UTF-8はMUST、名前の一意性はSHOULD、BOMの無視はMAY——この強度差がそのまま「破ると相互運用が壊れる/正当化すれば逸脱できる/完全に任意」の境界になる(RFC 2119の読み方が効く)。最後に、手元でJSONを整形・検証したいときは、正規表現で自作するより検証済みのツールに任せるほうが速く確実だ(実用ツール集にブラウザ完結のものを置いている)。
まとめ
RFC 8259は、16ページでJSONの文法を確定させ、STD 90として標準化した簡潔な仕様だ。ただし本当の勘所は、仕様が「あえて決めていない」空白——数値の精度、重複キーの勝者、日付やコメントの不在——にある。文法を覚えるだけでは本番のバグは防げず、UTF-8はMUST、名前の一意性はSHOULDといった規範語の強度と、IEEE 754倍精度という相互運用の前提を押さえて初めて、JSONを安全に運べる。さらに厳密さが要るならI-JSON(RFC 7493)へ進むとよい。他のRFC解説はRFC精読から辿れる。
RFC精読の記事ガイド
RFC 8259: JSONを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
RFC
比較で見る軸
難易度: advanced / カテゴリ: RFC精読 / タグ数: 5
導入後に効く点
数値文法は任意精度だが、相互運用はIEEE 754倍精度を想定し、2^53−1超の整数はずれうる。大きなIDは文字列で運ぶ。NaN・Infinity・先頭ゼロ・先頭の+は文法外。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- RFC精読
- タグ数
- 5
判断チェックリスト
- 自社の用途が「RFC / JSON」に近いか確認する。
- 強みである「RFC 8259はJSONの現行仕様でSTD 90。4627→7159→8259と改訂され、7159以降は数値や文字列単体も正しいトップレベル値になった。仕様本文は16ページである。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。