JSON 数字精度(JSON Number Precision)指的是:JSON 文本里的数字在语法上可以写得很长,但被大多数语言的解析器读进内存时,会被塞进一个固定宽度的数值类型,超出部分被悄悄截断或四舍五入,于是“读到的数”和“写的数”不是同一个数。
问题出在只有一种数字类型
JSON(JavaScript Object Notation)规范里,number 只有一种写法:可选负号、整数部分、可选小数部分、可选指数部分。它没有 int 和 float 之分,也没有规定最多能写几位。
麻烦在于具体实现。JSON 来自 JavaScript,而 JavaScript 只有一种数字类型 Number,对应 IEEE 754 双精度浮点(binary64):52 位显式尾数加 1 位隐含位,共 53 位有效二进制位,因此能精确表示的最大整数是 2^53−1,也就是 9007199254740991。超过这个范围,相邻整数之间的间隔就大于 1,能表示的数开始“跳着走”。
打个比方:双精度浮点像一把刻度固定的尺子,量 1 米、2 米很准;但你想表示 9007199254740993,这把尺子上根本没有这个刻度,只能落到最近的刻度上。
两个典型的坑:
1. 大整数 ID。雪花 ID、订单号、数据库自增主键动辄 18~19 位,远超安全整数范围。9007199254740993 经过 JSON.parse 会变成 9007199254740992。整个过程不报错、不告警,末位就变了。这类 bug 最阴险的地方是程序不崩,只是偶尔“查不到这个用户”。
2. 小数尾差。0.1 和 0.2 在二进制里是无限循环小数,存进双精度浮点只能取近似值,0.1 + 0.2 得到 0.30000000000000004。这里要分清:JSON 文本里的 0.1 本身是准确的十进制字符串,误差发生在解析成二进制浮点的那一刻,以及之后每一次运算和四舍五入。
和相邻概念的区别
- 与“浮点误差”:浮点误差是二进制表示的固有问题,任何语言都有;JSON 数字精度问题则是“传输格式 + 解析器默认类型”共同造成的,换个类型就能避免。
- 与“序列化”:序列化是双向的。写出去时,JavaScript 的
JSON.stringify也会把已经失真的值原样输出,问题在上游就已经发生。 - 与 JSON Schema:Schema 里 number 同样是笼统类型,不加约束就管不住精度。
Python 是个反例:json.loads 把整数解析成任意精度的 int,大整数不丢;但小数仍然是 float。
怎么办
| 场景 | 容易踩坑的做法 | 稳妥做法 |
|---|---|---|
| 大整数 ID | 当数字传 | 全程当字符串传,用时再转 |
| 金额 | 用浮点存“元” | 用整数存“分”,或用十进制类型(Decimal) |
| 跨语言接口 | 依赖默认解析 | 显式约定字段类型,必要时用支持任意精度的解析器 |
一句话记住:JSON 传输的是文本,精度损失发生在解析器选类型的那一刻。凡是“不能变”的数——ID、金额、时间戳——就别指望默认解析,把它当字符串或十进制类型处理。
