金額・ID・日時がAPIを通ると壊れる理由:JSONの精度と部分更新の契約
DBでは正しい値が、ブラウザや更新APIで変わってしまう。受注アプリを例に、金額の丸め、大きなID、業務日と瞬間、未指定とnullを4件のTipsから解説します。
金額・ID・日時がAPIを通ると壊れる理由
架空の受注アプリで、DBでは正しい注文IDなのにブラウザのリンク先が違う。合計に端数が残り、深夜の注文が前日の売上へ入り、表示名の変更で通知までオフになる。どれもJSONの構文エラーとは限りません。保存型、転送形式、クライアントの型、業務上の意味を1本の契約として確認します。
DBの列と制約 → APIの値・単位・形式 → クライアントの解析と変数 → 画面の表示・部分更新 → サーバーで再検証して保存
金額は浮動小数点の誤差と業務上の丸めを分ける
2進浮動小数点では0.1などの10進小数を正確に表せず、JavaScriptの0.1+0.2は0.30000000000000004になります。金額は最小単位の整数か、NUMERIC/DECIMALのような10進数型を選びます。ただし整数にも桁の上限があり、APIを通す際の安全範囲まで確認します。表示だけ丸めても、中間の比較や合計は直りません。
通貨コードと単位を一緒に定義します。USDの10.99ドルは1,099セント、JPYの500円は500円です。これはStripeの一般的な最小単位の例ですが、通貨や操作ごとの例外も公式仕様で確認します。独自APIでは金額の最小単位を10進整数の文字列として運ぶ契約も選べます。文字列だから何でも許可せず、符号・桁数・上限を検証します。
割り算の余りは別の業務判断です。1,000セントを3人へ分け、各333セントにすると999セントです。独自の配分規則で最初の1人へ1セントを足し、334・333・333とすれば合計は1,000になります。この順序に意味があるか、配分の公平性が必要かも決めます。10進数型に変えるだけでは配分ルールは決まりません。
大きな整数IDは、丸められる前に文字列で渡す
RFC 8259は数値の範囲と精度に実装上の制限を認め、広く一致できる整数範囲として±(2^53−1)を挙げます。JavaScriptのNumberで9007199254740993を読むと9007199254740992へ丸まります。安全範囲を超える整数の一部は表現できますが、連続する整数を区別できる保証は失われます。
JSON.parse('{"id":9007199254740993}').id
// 9007199254740992
JSON.parse('{"id":"9007199254740993"}').id
// "9007199254740993"JSON.parseの後でStringへ変換しても、既に失われた桁は戻りません。IDは計算対象にせず、中身を数値へ変換したり分解したりせず、文字列のままURL・比較・ログへ引き継ぎます。このように中身を解釈せず扱う値を、不透明な識別子と呼びます。数値を許す既存APIを移行する場合も、丸まった入力を元IDだと推測して補正せず、対応クライアントと形式の切り替えを管理します。
日時は瞬間、請求日や誕生日は日付として扱う
2026年10月27日00:30 JSTは、UTCでは10月26日15:30です。同じ瞬間でも日付は地域で変わります。日本の27日の売上は、JSTの27日0時から28日0時をUTCへ変換し、開始時刻を含み、終了時刻を含まない範囲(半開区間)で読みます。誕生日のような日付だけの値をUTCの深夜へ変換すると、別地域で前日になる不具合を作ります。
created_at >= $1::timestamptz
AND created_at < $2::timestamptz
-- $1 = 2026-10-26T15:00:00Z
-- $2 = 2026-10-27T15:00:00ZPostgreSQLのtimestamptzは瞬間を扱い、入力に使った地域名をそのまま保持しません。毎日9時の予約なら、瞬間だけでなく地域の時刻規則も必要です。夏時間がある地域の翌日境界は、UTCで24時間を足すのではなく地域の日付から作ります。APIでは瞬間にオフセットを要求し、日付だけの値はYYYY-MM-DDの契約にします。
部分更新では未指定・null・falseを区別する
| 入力 | このAPIでの意味 | 検証 |
|---|---|---|
| 項目がない | 現在値を保持 | デコード時に存在を残す |
| false・0・空文字 | 明示した値 | 許容する値かを検証 |
| null | 採用する形式で意味を定義 | Merge Patchならメンバー削除 |
| 未知または更新禁止の項目 | 拒否 | 認可と許可項目を確認 |
JSON Merge Patchではnullはメンバー削除、配列は全体の置き換えです。JSON Patchならremoveやreplaceの操作を明示できます。PATCHというメソッドだけではこの意味は決まりません。どちらでも更新可能な項目を制限し、適用後の業務状態を検証します。同時更新があるなら版の条件も別に設計します。
注文APIの契約を1つの例にまとめる
{
"id": "9007199254740993",
"amount_minor": "1099",
"currency": "USD",
"created_at": "2026-10-26T15:30:00Z",
"billing_date": "2026-10-27",
"notifications_enabled": true
}このJSONなら注文IDと金額の桁、瞬間と請求日の意味を分けて受け取れます。ただし入力の型が合うだけでは正当な注文にはなりません。IDへの認可、金額のサーバー側計算、通貨との対応、更新の条件を確認します。契約の変更時は保存済みデータや古いクライアントとの互換性も調べます。
境界値をクライアントまで通して確かめる
- 安全な整数の境界付近と64ビットIDを、保存・JSON・ブラウザ・更新要求まで往復させる。
- 配分の余りと丸めの境界を試し、明細合計と注文合計の契約を確認する。
- JSTの日付境界と夏時間の境界を別に試し、サーバーの地域設定を変えても結果が同じか確認する。
- 未指定・null・false・0・空文字・配列を別々の部分更新として試し、無関係な項目が変わらないか確認する。
参考にした一次資料
以下の公式資料は2026年10月27日に確認しました。数値例とシナリオは独自の仮定です。設定例は説明用で、実サービスでの実行や性能測定は行っていません。
- PostgreSQL 18 — Numeric types
- Stripe — Currency minor units
- RFC 8259 — JSON numbers
- PostgreSQL 18 — Date/time types
- RFC 7396 — JSON Merge Patch
- RFC 6902 — JSON Patch