JSONの大きな数値が変わる理由|2⁵³とBigInt
2026.10.03 更新 · 5分で読めます
JSONの大きな整数IDがJavaScriptで下の桁が変わってしまう理由を、IEEE 754倍精度と2⁵³の限界から説明します。IDを文字列で受け渡す方法と、BigIntで読み取る方法もまとめました。
JSON整形・検証をすぐに使うサーバーが返した12345678901234567890が画面で12345678901234567000になっていても、JSONが壊れているわけではありません。JavaScriptがすべての数値を64ビット浮動小数点数(IEEE 754倍精度)として読み取るためで、整数が正確なのは2⁵³−1(9,007,199,254,740,991)までです。確実な対策は、大きなIDを文字列で受け渡すことです。手元のJSONにこうした数値があるかどうかは、JSON整形・検証で確認できます。数値を書かれたとおりに保ち、JavaScriptで読み込むと変わる数値がいくつあるかを表示します。
何が起きているのか
JSON.stringify(JSON.parse('{"n":12345678901234567890}'));
// '{"n":12345678901234567000}'
JSON.parse("9007199254740993");
// 9007199254740992
エラーも警告も出ず、値だけが変わります。注文番号や投稿IDのように64ビット整数で採番した値がこのように変わると、検索しても見つからなかったり、別のレコードを指したりします。
なぜ2⁵³なのか
JavaScriptのNumberは、IEEE 754の倍精度(binary64)形式です。64ビットのうち符号が1ビット、指数が11ビット、仮数が52ビットで、仮数の先頭にある暗黙の1ビットを合わせると精度は53ビットになります。そのため2⁵³までの整数はすべて正確に表せますが、2⁵³から2⁵⁴の間では偶数しか表せず、数が大きくなるほど間隔が広がります。9007199254740993が9007199254740992になるのはこのためです。
| 値 | 意味 |
|---|---|
| 9,007,199,254,740,991 | 2⁵³−1。Number.MAX_SAFE_INTEGER |
| 9,007,199,254,740,992 | 2⁵³。ここから先は隣り合う整数を区別できない |
| 9,223,372,036,854,775,807 | 2⁶³−1。符号付き64ビット整数の最大値(19桁) |
2⁵³は16桁の数です。17桁以上の整数は必ずこの範囲を超え、16桁の場合は9,007,199,254,740,991以下かどうかを確認する必要があります。
標準ではどうなっているか
RFC 8259 6節の数値の文法には、桁数の制限がありません。そのため12345678901234567890は正しいJSONです。一方で同じ節は、実装が数値の範囲と精度に制限を設けることを認めており、IEEE 754 binary64を超える精度を期待しなければ相互運用性が高くなると述べています。整数は−(2⁵³)+1から2⁵³−1までが、実装どうしで値が正確に一致する範囲です。
つまり、問題は読み取る側で起こります。64ビット整数型や任意精度の整数として読み取る言語では値が変わりませんが(上の例は20桁なので、符号なし64ビットの範囲です)、JavaScriptのJSON.parseはNumberとして読み取ります。同じ節が相互運用性の問題の例に挙げている1E400は、JSON.parseではInfinityになり、JSON.stringifyに渡すとnullになります。
小数と表記も変わる
1.0は、JSON.parseで読み取ってから書き出すと1になります。値は同じですが表記が変わるため、元のテキストを比較したり署名したりする場合に問題になります。- 桁数の多い小数は、最も近い倍精度の値に変わります。
0.1234567890123456789は0.12345678901234568になります。 - 金額のように正確さが必要な値は、文字列か最小単位の整数(円、セントなど)で送るほうが確実です。
値を変えないための方法
IDは文字列で送ります。 {"id":"12345678901234567890"}のようにダブルクォートで囲めば、どのパーサーでも変わりません。IDは計算に使わないので、数値にしておく理由はありません。サーバー側のシリアライズ設定で、64ビット整数を文字列として出力するようにしてください。
計算が必要ならBigIntを使います。 BigInt("12345678901234567890")は精度を失いません。ただしJSON.stringifyはBigIntを渡されるとTypeErrorを投げるため、出力するときはtoString()で文字列にします。
数値のまま受け取る必要があるなら、元のテキストを参照します。 TC39の「JSON.parse source text access」提案(ステージ4)では、reviver関数の第3引数で数値の元のテキストを受け取れます。2026年10月時点で、Node.js 24.14.0で次のコードが動くことを確認しました。対応していない実行環境ではcontextが渡されないため、先に確認してください。
const data = JSON.parse('{"n":12345678901234567890}', (key, value, context) =>
typeof value === "number" && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source)
? BigInt(context.source)
: value,
);
// data.n === 12345678901234567890n
中身を確認・整形するときにも注意します。 JSON.parseで読み取ってから書き出すフォーマッターは、整形するだけで値を変えてしまいます。ZEKILO DevのJSON整形・検証は数値を計算せず、文字のまま出力します。{"n":12345678901234567890}を整形しても値は変わらず、「JavaScriptで読み込むと精度が変わる数値が1個あります。ここでは書かれたとおりに保持します。」と表示します。1.0も1.0のままです。
まとめ
- JSONの標準には数値の大きさの制限がありませんが、2⁵³−1より大きい整数は、JavaScriptで正確に扱えるとは限りません。
- 17桁以上の整数IDは、文字列で受け渡します。
- JavaScriptで大きな整数を扱うときはBigIntを使い、出力するときは文字列にします。
- JSONを目で確認するときは、数値を書き換えないJSON整形・検証を使ってください。