
订单号在服务端是合法的 int64,到了浏览器却少了一位;请求仍成功,只是更新错了记录。根因常在“JSON 数字如何解析”没有写进接口契约。先定清取值范围、语义和编码,才能避免静默的数据损坏。
精度是在解析时丢失的
JSON 允许很长的十进制整数,接收端却未必有等宽的数值类型。浏览器中的 JavaScript Number 使用 IEEE 754 双精度浮点数,只有 -9007199254740991 到 9007199254740991 之间的整数可以逐个精确表示。服务端常见的 64 位整数则远大于这个范围。
| 表示方式 | 可精确表示的整数范围 | 适合的字段 |
|---|---|---|
JavaScript Number |
±(2^53 - 1) |
页码、毫秒级短时长、普通计数 |
| 有符号 64 位整数 | -2^63 到 2^63 - 1 |
数据库主键、序号、时间戳 |
| 十进制字符串 | 由协议定义 | 跨语言 ID、金额最小单位、大整数 |
例如,下面的值在 JSON 文本里没有问题,但 JSON.parse 返回前已经发生了舍入;之后再转成字符串或 BigInt 都无法恢复原值:
1 | const payload = JSON.parse('{"orderId":9007199254740993}') |
不要用“前端通常不会遇到这么大的 ID”作为约束。自增主键和雪花类标识都会自然越过安全整数边界,且问题往往在系统运行一段时间后才暴露。

按字段语义设计编码
最稳妥的规则不是“所有数字都转字符串”,而是先区分字段用途。只用于展示和相等比较的标识符是不可拆分的令牌,应始终编码为字符串。参与计算的量必须明确单位、范围、溢出策略和客户端类型;金额可用最小货币单位的整数,避免让浮点数承担精确结算。
推荐把不安全的整数在 JSON 中直接写成十进制字符串,并在接口文档中标注格式:
1 | { |
这里 orderId 是不透明 ID,客户端不应对它加减;createdAtMs 可能越界时也应传字符串;amountMinor 只在双方确认范围较小时保留 JSON 数字。字段名中的单位能减少“元还是分”“秒还是毫秒”的另一类隐患。
在边界处做严格校验
字符串不是放弃类型,而是把解析推迟到拥有正确类型的边界。客户端需要大整数运算时,先校验十进制格式,再构造 BigInt;服务端也应拒绝空串、小数、科学计数法和越界值。
1 | function parseUnsignedId(value: string): bigint { |
不要在通用反序列化器里悄悄把字符串 Number(value)。应把转换集中在 DTO、请求校验器或领域适配层,并为临界值建立契约测试:2^53 - 1、2^53、int64 最大值、前导零、负数和非法字符都要覆盖。若接口已发布数字 ID,迁移时可以先新增字符串字段并双写,确认消费者切换后再废弃旧字段,避免一次改动打断所有客户端。

把数字当作协议的一部分
整数精度问题难排查,是因为它通常不抛异常:日志里只是一个“看起来合理”的数字。接口评审时应为每个数值字段写下语义、单位、允许范围和 JSON 编码,再让服务端、浏览器、移动端 SDK 与数据库往返测试同一组边界样例。这样,64 位整数不再是某个语言的内部细节,而是所有消费者都能执行的协议承诺。