JSON 大整数精度陷阱:雪花 ID 被静默改写时怎么查
症状:ID「差一点点」
联调里一类难查的 bug:
- 后端日志里订单号 / 用户 ID 是
2099774875233849345 - 前端、格式化工具或复制进控制台后再看,变成了
2099774875233849400 - 接口签名、对账、按 ID 回查全部失败,但
JSON.parse并不报错
这不是网络截断,也不是「格式化改了缩进」。是 IEEE 754 双精度在解析数字字面量时丢了低位。
为什么合法 JSON 也会丢精度
JSON 规范把「数字」定义为十进制字面量,没有规定实现必须用任意精度整数。浏览器与 Node 的 JSON.parse 把数字映射到 JS number(双精度),只有不超过 Number.MAX_SAFE_INTEGER(2^53−1 = 9007199254740991)的整数才能保证逐位还原。
| 位数(约) | 例子 | 风险 |
|---|---|---|
| ≤15–16 | 普通自增 ID | 通常安全 |
| 18–19 | 雪花 ID、部分分布式 ID | 高风险静默舍入 |
| 带小数 / 科学计数 | 1e20、0.1 | 另有浮点语义,不在本文焦点 |
对照实验(在浏览器控制台):
JSON.parse('{"id":2099774875233849345}').id
// → 2099774875233849400
String(JSON.parse('{"id":2099774875233849345}').id) === '2099774875233849345'
// → false
Number.isSafeInteger(2099774875233849345) // → false
雪花一类 ID 把时间戳、机器位、序列号塞进约 64 bit,位数刚好落在 18–19 位危险区——binary64 的尾数(约 53 bit)无法表示每一个整数。自增 ID 若长期停在约 16 位以内通常无事;一旦分布式 ID 越过安全边界,所有未经字符串化的 JS 消费端都会静默改写。
语言对照:谁会舍入
| 运行时 / 库 | 默认数字类型 | 大整数命运 |
|---|---|---|
浏览器 / Node JSON.parse | IEEE 754 双精度 | 超过 2^53−1 静默舍入 |
Python json | 任意精度 int | 位数保留 |
Go encoding/json → float64 | 双精度 | 与 JS 同类陷阱 |
Go → json.Number / int64 | 文本或 64 位整型 | 类型选对则保留 |
Jackson → long | 64 位 | 有符号 long 内通常 OK;若再经 JS 仍会坏 |
事故常被归因为「只有前端有问题」,其实后端内存里可能一直是正确的 64 位值。中间代理若用双精度再序列化一次,报文在到达浏览器前就已经坏了。每一个调用标准 JSON 解析器的跳点都值得怀疑。
三条出路(按可靠性)
-
协议层:ID 用字符串
{"id":"2099774875233849345"}—— 跨语言最稳,也是多数团队的长期约定。 -
调试层:无损解析
格式化/展示时不要用裸JSON.parse。本站 JSON 格式化工具 对格式化与压缩走parseLossless:数字 token 以原文形式保留,再stringify回去。适合「先看清报文再改代码」。 -
应用层:BigInt / 十进制类型
仅在你控制两端运行时且明确协议时使用;JSON 文本本身仍常以字符串承载,避免中间代理再 parse 一次。
和站内其他路径的关系
| 场景 | 风险 | 互链 |
|---|---|---|
| API 响应里的雪花 ID | 前端 JSON.parse | 本文 + 格式化指南 |
| JWT claims 里的大整数 | 解码后再 parse | JWT 解码指南 |
| Base64 解出嵌套 JSON | 编码无损、解析仍可能损 | Base64 指南·大整数节 |
| 两份 JSON Diff 数字对不上 | 可能是解析弄坏而非业务变更 | 排错手册·第四类 |
实现注记
无损路径用手写递归下降解析器识别数字 token,装进 LosslessNumber{ raw },序列化时打印 raw 而不是 Number(raw)。合法性「校验」若只需 true/false,可以仍用标准 JSON.parse——那只回答「能不能 parse」,不回答「值有没有被改」。签名、哈希、对账场景必须以字符串或无损树为准。
快速自检清单
- 把可疑 ID 粘进 JSON 格式化,看输出数字串是否与原文逐位相同。
- 若格式化已保全、自己代码里却变了——在业务代码里搜裸
JSON.parse/response.json()。 - 推动后端把 ID 字段改为字符串;短期前端可用字符串或 BigInt 策略兜底。
- 不要用「再 format 一次」当修复——劣质工具会把错误固化进剪贴板。
排查对照:是精度还是别的问题
| 你看到的现象 | 更像 | 下一步 |
|---|---|---|
| ID 末几位变成 0 或整百 | IEEE 754 舍入 | 本文无损路径 / 改字符串 ID |
整段变成 null / 缺字段 | 字段名或序列化配置 | Schema / Diff |
中文变 \uXXXX | ASCII 转义策略 | Unicode 转义文 |
Unexpected token | 语法 | 排错手册 |
| Diff 显示数字不同,但业务没发版 | 一侧用了有损 parse | 两侧都走无损或字符串后再比 |
具体链路:Base64 → JSON → JWT claim
常见流水线:服务 A 把 JSON 做 Base64,服务 B 解出字节再 JSON.parse,某个字段随后写进 JWT payload。Base64 不会改数字字符——损伤发生在 parse。若只在 Base64 工具里看原文,ID 仍正确;任一跳把值落成 JS number,低位就没了。排查顺序:先用无损格式化核对 JSON token → 看 JWT 解码把 claim 显示成字符串还是数字 → 再怀疑传输层。
金额、汇率、库存小数是另一类问题(十进制 vs 二进制浮点),不要和「整数 ID 被改写」混为一谈。ID 用字符串;金额用整数最小货币单位或 decimal 字符串——两条纪律可以同时成立。
FAQ
浮点数也会被无损路径「原样」保留吗?
原文 token(如 0.50、1e5)会按字面保留;这不等于十进制算术精确。金额字段仍应用整数分或 decimal 字符串协议。
这是 JSON 规范的 bug 吗?
规范把数字留给实现。ECMA-404 / RFC 8259 不要求任意精度。问题出在「用双精度承载业务 ID」的工程选择,不是缩进风格。
和「校验通过」矛盾吗?
不矛盾。校验只问语法;精度问语义。语法绿灯 + ID 错位,是本陷阱的标准画像。
本文由 ToolVault 工具匣 提供。更多开发者工具请访问 首页。
相关工具
相关文章
JSON 解析报错 Unexpected token?5 种常见原因和修复方法
JSON 解析报错 Unexpected token?本文总结 5 种最常见的 json 格式错误原因:尾逗号、单引号、未转义字符、BOM 头、注释,逐一给出修复方法和代码示例。
JSON 排错完全手册:从报错信息到修复(按症状速查)
JSON 报错看不懂?本手册按「症状 → 原因 → 修复」组织,覆盖语法错误、中文转义、编码乱码、Schema 校验失败四类高频问题,附诊断决策树与通用排查流程。
JSON Schema 校验报 "required" 字段缺失,怎么定位修复?
JSON Schema 校验提示 required 字段缺失、类型不匹配怎么办?讲清 required / additionalProperties / type 的常见坑,并用本站工具逐步定位是哪一层出了问题。