⏱ 読了目安: 約11分
- 事実と背景:JSON標準規格(RFC 8259等)には数値精度やキー重複などの曖昧性が残り、実装間差異が存在。
- 技術的変革:PythonやRubyは多倍長、Go v2やBigQueryは厳格エラーや前勝ちを採用し、処理系ごとの解釈が乖離。
- 現場への影響:異種システム間のデータ連携でサイレントデータ破損が発生するリスクがあり、I-JSON等の厳格な運用が必須。
数値表現の暗黙ルールと精度消失の罠
深夜2時、障害通知アラートのけたたましい音で目を覚まし、ダッシュボードを開くと、マイクロサービス間でやり取りされた決済ログの金額が微妙に食い違っている——エンジニアであれば一度はこのような血の凍るような経験があるかもしれない。問題の原因を追跡していくと、特定のサービスが受け取ったJSON内の巨大なIDや小数の数値が、型変換の過程で勝手に丸められていたことが判明する。我々が日常的に空気のように使っているJSON(JavaScript Object Notation)は、一見シンプルで汎用的に見えて、その裏には数々の「意味論的な曖昧さ」が潜んでいる。
JSONの標準規格であるIETF RFC 8259やECMA-404は、数値リテラルの構文(文法)を定義しているものの、その数値がプログラム上でどのように解釈・保持されるべきかという意味論までは明確に規定していない。標準仕様では「多くの実装がIEEE 754 binary64(64ビット浮動小数点数)を使用している」と言及し、相互運用性を高めるために safe integer 範囲(-(2^53)-1 から 2^53-1)での運用を示唆してはいるものの、これは強制力のある規定(MUST)ではない。その結果、プログラミング言語やデータベースの実装ごとに数値の扱いが大きく乖離することになる。
例えば、1.0 と 1 という表現を考えたとき、JavaScriptやPostgreSQL(jsonb型)はこれらを構文的に区別せず扱う。JavaScriptではすべての数値が内部的にnumber(64ビット浮動小数点数)として処理され、PostgreSQLは任意精度のnumericとして保持する。一方で、PythonやRubyは小数点の有無でfloatと多倍長整数(int)を厳格に区別し、Go言語のencoding/jsonでは受け取る構造体の型定義に従って型チェックを行う。さらにRustのserde_jsonやGoogle BigQueryのJSON型では、小数点の有無だけでなく数値の大きさや符号の有無によって内部的な型(f64、i64、u64など)が動的に分岐する仕組みになっている。
さらに危険なのが、オーバーフローや丸め処理の挙動の違いだ。1e400のような倍精度浮動小数点数の限界を超える巨大な小数が入力された場合、JavaScript、Python、RubyはエラーにせずInfinity(無限大)へとサイレントにオーバーフローさせる。これに対し、Go(encoding/jsonおよびv2)、Rust(serde_json)、BigQueryは即座にデコードエラーを発生させる。一方でPostgreSQLのjsonbは10進の多倍長小数としてそのまま正確に保持するため、エラーも発生させず精度落とも生じない。また、-0.0のような「負のゼロ」の扱いにおいても、JavaScriptやPython、Go、Rustが正のゼロと区別して保持するのに対し、PostgreSQLは同一視して正のゼロに統合する。
| 言語・DB | 整数と小数の区別 | 整数の範囲制限 | オーバーフロー(1e400) | 負のゼロ(-0)の保持 |
|---|---|---|---|---|
| JavaScript (JSON.parse) | 区別なし (全てnumber) | safe integer範囲推奨 | Infinity に丸める | 保持する |
| Python (json) | 区別あり (int/float) | 多倍長 (制限なし) | Infinity に丸める | 0扱い (整数) / 保持 (float) |
| Go (encoding/json/v2) | 受け取り型に依存 | 固定長外はエラー | エラーを返す | 保持する |
| Rust (serde_json) | 区別あり (i64/u64/f64) | 固定長外はf64化 | エラーを返す | f64として保持 |
| PostgreSQL (jsonb) | 区別なし (numeric) | 多倍長 (制限なし) | 多倍長小数で正確保持 | 正のゼロと同化 |
| BigQuery (JSON) | 条件によりFLOAT64化 | INT64/UINT64外はFLOAT64 | エラーを返す | 0扱い (整数) / 保持 (float) |
このように、マイクロサービスアーキテクチャにおいてPythonで生成したJSONをGoやBigQueryで受け取ったり、PostgreSQLを経由してJavaScriptフロントエンドに渡したりする際、数値の精度欠落や不意のエラーがいつでも発生し得る。我々開発者は、「JSONだからどの環境でも同じようにパースできるはずだ」という無邪気な前提を今すぐ捨てなければならない。
孤立サロゲートが招く文字化けと脆弱性
文字コードのパース処理において、デッドロックやスパゲッティコード以上にエンジニアを悩ませるのが「文字化け」や「不正なバイト列の混入」だ。JSONにおける文字列の扱いもまた、規格間の微妙なギャップによって重大なバグや脆弱性の温床となっている。
ISO/IECおよびEcma InternationalのJSON規格(ECMA-404)において、JSONの文字列は単純に「Unicode code pointの列」と規定されている。ここには、上位サロゲートや下位サロゲートが単独で存在する「孤立サロゲート(Isolated Surrogate)」を排除する規定が存在しない。一方で、インターネット標準であるIETFのRFC 8259では、ネットワーク上のデータ交換フォーマットとしてJSONを入力する際、テキストが「UTF-8でエンコードされていること」を明確に義務付けている(MUST)。UTF-8仕様においては、孤立サロゲートのような不正なUnicodeコードポイントを正しく表現することができないため、ここに規格間の明確な捻れが発生する。
実務で問題となるのは、JavaScriptやPythonのように内部文字列表現にUTF-16やUnicodeコードポイント配列を採用している言語と、GoやRust、データベースのように内部表現をUTF-8で統一している環境との間でのデータ連携だ。例えば、JavaScript環境で "uD800" というエスケープされた孤立上位サロゲートや、生の孤立サロゲートを含むJSON文字列を作成した場合、JavaScriptの JSON.parse はエラーを出さずに孤立サロゲートを含む文字列オブジェクトを生成する。Python(PEP 393以降のstr型)においても同様であり、孤立サロゲートはそのままstr内部に保持される。
しかし、このデータをUTF-8ベースのシステムへ送出すると事態は一変する。Go言語の次世代ライブラリであるencoding/json/v2や、Rustのserde_json、RubyのJSONモジュール、PostgreSQLのjsonb、BigQueryのJSON機能は、デコード時に孤立サロゲートのエスケープシーケンスに遭遇すると即座に「不正なUnicode」としてエラーを返して処理を拒否する。一方で、Goの従来のencoding/jsonライブラリは、不完全なUTF-8や孤立サロゲートに対して比較的緩いバリデーションしか行わず、Unicodeの置換文字である U+FFFD ()に勝手に置き換えて処理を継続してしまう。
この差が生み出すリスクは、単なる表記の乱れにとどまらない。例えば、ユーザーの入力データやファイル名、外部APIから取得した文字列に奇妙な孤立サロゲートが含まれていた場合、あるマイクロサービス(Python製)では正常に受け入れられ、データベース(PostgreSQL)への保存時に突然クラッシュするか、ログ収集基盤(BigQuery)へのデータ同期時にサイレントに転送が失敗するという事故が頻発する。さらに、エスケープされたサロゲートペア "uD800uDC00" が入力された際、追加面のコードポイント U+10000 へ自動的に合成展開されるかどうかも実装によって挙動が異なり、セキュリティ上の不整合やインジェクション脆弱性の引き金になり得るのだ。
重複キーと順序依存に潜むマルチクラウドの死角
API設計やデータパイプラインの構築において、最も見落とされやすく、かつ大規模障害を引き起こしやすいのが「JSONオブジェクトにおけるキーの重複」と「キーの評価順序」だ。
JSON仕様において、同一オブジェクト内で同じキーを複数回記述すること(例: {"role": "user", "role": "admin"})について、IETF RFC 8259は「キーは一意であるべき(SHOULD)」としながらも、重複が存在した場合の解釈について明確な禁止をしていない。仕様内では、世の中の実装が「最後の値を有効にする(後勝ち)」「エラーを出す」「すべての値を記録する」という3つの異なるアプローチに分かれていることを事実として追記しているのみだ。ECMA-404に至っては、重複キーに関する要求事項を何ら定めていない。
実際の実装を比較すると、この挙動の分裂は極めて深刻だ。JavaScript、Python、Ruby、Go(v1)、PostgreSQL(jsonb)の標準的なパーサーは、同一キーが存在した場合に「後勝ちな(最後の値を採用する)」挙動を示す。これに対し、Google BigQueryのJSON型はデフォルトで「前勝ち(最初の値を採用する)」という対極の動作を行う。さらに、Go言語の新たな標準候補であるencoding/json/v2や、Rustのserde_jsonでTyped Struct(構造体)へマッピングする場合は、重複キーが存在した時点でデコードエラー(例外)を発生させる。
この「後勝ち vs 前勝ち vs エラー」の相違は、認証システムや権限管理において致命的なセキュリティホールを生み出す。攻撃者がリクエストパラメータに {"is_admin": false, "is_admin": true} という重複キーを仕込んだ場合、Appサーバー(Node.js/Python)では is_admin: true として評価され管理者権限が奪取される一方で、セキュリティ監査ログ(BigQuery)には is_admin: false として記録されるような、監査を逃れるサイレント攻撃が可能になってしまうのだ。
また、キーの「順序性」についても同様の死角が存在する。JSON仕様ではオブジェクトのキー順序に意味を持たせていないが、現実のエコシステムではキーの順序に依存する設計が広く定着している。代表例がNode.jsや各種バンドラー、npmパッケージ配布の基盤となっている package.json の exports フィールドだ。Node.jsのモジュール解決ロジックは、exports 内のキーの出現順序(上から順にマッチングを試みる)に厳格に依存している。
しかし、JSONパーサーの多くはキーの順序を保持しない。Goのencoding/jsonはハッシュマップに読み込むためキーの順序が非決定論的にシャッフルされ、PostgreSQLのjsonbやBigQueryのJSONは内部ストレージの最適化のためにキーを自動的にソートして格納する。このため、JSONデータを一度データベースに保存して出力したり、Go製のCI/CDツールを経由して再フォーマットしたりした瞬間に、package.json の順序依存ロジックが破綻し、本番環境でのみサードパーティライブラリの読み込みに失敗するようなスパゲッティ状態のバグが誘発されるのである。
現場が明日から取るべき「JSON安全運用」の処方箋
ここまで見てきた通り、JSONは「極めてシンプルでどこでも動く共通言語」という幻想の裏に、数々の実装依存の罠を隠し持っている。マルチ言語化が進み、AWS、GCP、各種SaaSを組み合わせた複雑な分散システムを構築する現代の開発現場において、我々シニアエンジニアはどのような対策(処方箋)を打つべきなのだろうか。
第一に、システム間の境界を流れるJSONメッセージに対しては、曖昧性を削ぎ落としたサブセット規格である「I-JSON (RFC 7493)」のプロファイル運用を徹底することだ。I-JSONでは、数値の範囲を IEEE 754 safe integer(-(2^53)+1 から 2^53-1)に制限し、トップレベルオブジェクトのキー重複を禁止(MUST NOT)し、文字列のエンコーディングを厳格なUTF-8に限定している。API設計時には、巨大なIDや精密な金額データを数値リテラルとして直接渡すのをやめ、常に文字列(String)型としてエンコードして渡す設計を標準化すべきである。
第二に、CI/CDパイプラインやAPIゲートウェイのレイヤーにおいて、厳格なスキーマバリデーションとリンターを自動実行する仕組みを組み込むことだ。Go encoding/json/v2 のような重複キーや不正サロゲートを拒否する厳格なパーサーをエッジ側で採用し、フォーマット違反のデータを早期にドロップさせる構成が望ましい。また、キーの順序にビジネスロジックを依存させる構造(前述の exports パターンなど)はアンチパターンと定義し、配列(Array)構造へとリファクタリングを推進すべきだ。
我々エンジニアが直面している本質的な問題は、テクノロジーの抽象化が進んだ結果、基礎的なデータ表現のレイヤーで何が起きているか(パース処理や型キャストの泥臭い仕様)への洞察が失われつつあることではないだろうか。「とりあえずJSONで送っておけば大丈夫」という思考停止を脱し、データがシステムを通過する際の型変容や精度の変化にまで思いを馳せること——それこそが、堅牢なシステムを構築するための第一歩となるはずだ。あなたのチームのシステムでは、異種言語間でやり取りされるJSONの「1e400」や「重複キー」を、今日この瞬間も安全に裁けていると言い切れるだろうか?


コメント