JSONの数値精度
JSONの数値精度の限界、IEEE 754浮動小数点の問題、大きな整数が精度を失う仕組みを理解しましょう。JSONにおける金融データやIDの安全な取り扱い方を解説します。
詳細な説明
JSONの数値はRFC 8259で定義された構文に従います: オプションの符号、整数部、オプションの小数部、オプションの指数部です。重要なことに、JSON仕様は数値の範囲や精度に制限を課していません。しかし、実世界のパーサーはIEEE 754倍精度浮動小数点表現を使用しており、すべての開発者が理解すべき精度の限界があります。
IEEE 754倍精度の限界:
JavaScriptの Number 型(およびほとんどのJSONパーサー)は64ビットIEEE 754倍精度を使用し、以下を提供します:
- 整数精度: 2^53 - 1(9,007,199,254,740,991)まで正確に表現。
Number.MAX_SAFE_INTEGERとして知られる - 最大値: 約1.8 x 10^308
- 最小正値: 約5 x 10^-324
- 小数精度: 約15〜17桁の有効数字
整数精度の問題:
2^53を超える整数はJavaScriptのnumberとしてパースすると精度を失います:
JSON.parse('{"id": 9007199254740993}')
// Result: { id: 9007199254740992 } — off by one!
これは64ビット整数IDを使用するシステムにとって重大な問題です。Twitterはこの問題に遭遇し、APIレスポンスにツイートIDの数値表現と文字列表現の両方("id" と "id_str")を含めるようになった有名な事例があります。
小数精度の問題:
浮動小数点演算はよく知られた丸め誤差を生みます:
JSON.parse('{"price": 0.1}') + JSON.parse('{"price": 0.2}')
// Result: 0.30000000000000004
これはJSONの問題ではなく、IEEE 754表現の根本的な性質です。0.1や0.2のような数値は2進浮動小数点で正確に表現できません。
大きな整数の解決策:
- 文字列エンコーディング: 大きな整数を文字列として送信:
{"id": "9007199254740993"} - BigInt(JavaScript): reviver関数を使用して文字列IDをBigInt値に変換
- カスタムパーサー:
lossless-jsonやjson-bigintなどのライブラリは精度を失わずに大きな数値をパース
小数精度の解決策:
- 整数表現: 金額を最小単位(ドルではなくセント)で保存:
{"amount_cents": 1999}({"amount": 19.99}の代わり) - 文字列表現:
{"amount": "19.99"}は正確な小数表現を保持 - Decimalライブラリ:
decimal.jsやbig.jsなどのライブラリで任意精度の演算を使用
開発者がよくやるミス:
最も危険なミスは、浮動小数点の不正確さを考慮せずにJSON数値を金融計算に使用することです。1セント未満の丸め誤差が何百万件のトランザクションにわたって蓄積されます。もうひとつのミスは、Snowflake ID(Discord、Twitter、多くの分散システムで使用)などの大きな識別子に数値型を使用し、暗黙に精度を失うことです。また、末尾のゼロ付きで数値を送信し(1.50)、それが保持されることを期待する開発者もいますが、JSONパーサーは 1.50 を 1.5 に正規化します。
ベストプラクティス:
金額は最小の通貨単位の整数で表現してください。2^53を超える可能性のあるIDには文字列を使用してください。数値フィールドが整数か小数かをAPIスキーマでドキュメント化してください。Number.MAX_SAFE_INTEGER 周辺の境界値でテストしてください。数値フィールドの意図された精度をアノテーションするためにJSON Schemaの format キーワードの使用を検討してください。
ユースケース
決済APIでトランザクション金額を浮動小数点のドルではなく整数のセントとしてエンコーディングし、IEEE 754の丸め誤差による暗黙のデータ破損を防止する。