Javascript is required
踩坑避坑发布于 2026-07-28审校于 2026-08-088 分钟阅读

JSON 大整数精度丢失:Number 安全范围与解决方案

接口中的订单 ID、雪花 ID 或数据库 bigint 字段在原始响应里明明正确,经过 JSON.parse 后尾数却发生变化。问题不在 JSON 文本是否有效,而在解析结果被存入 JavaScript Number 时超出了安全整数范围。本文只讨论“大整数精度”这一问题,不讨论超大 JSON 的渲染性能或普通小数的财务计算。

JSONJavaScriptNumberBigInt精度丢失

一、最小复现:解析成功不代表数值准确

下面的 JSON 语法完全合法,JSON.parse 也不会抛出异常,但 orderId 的解析结果已经与原始数字不同。最危险的地方正是这种错误通常没有报错,只会在展示、比较或回传 ID 时暴露。

不要只用控制台输出后的数字判断接口是否正确。应同时查看 Network 面板中的原始响应文本,并用 Number.isSafeInteger 检查解析后的整数。

const raw = '{"orderId":9007199254740993,"count":42}';
const data = JSON.parse(raw);

console.log(data.orderId); // 9007199254740992
console.log(Number.isSafeInteger(data.orderId)); // false
console.log(data.count); // 42
console.log(Number.isSafeInteger(data.count)); // true

二、根因:JSON 数字与 JavaScript Number 不是同一层限制

JSON 使用十进制数字字面量表示数值;接收端采用什么数值类型,由具体实现决定。JavaScript 的 Number 使用 IEEE 754 binary64,整数在 -(2^53 - 1) 到 2^53 - 1 之间可以连续、精确地表示,这个上限可通过 Number.MAX_SAFE_INTEGER 获取。

超过安全范围后,并不是所有整数都会立即变成 Infinity,而是相邻整数可能被舍入到同一个可表示值。因此“位数看起来还在”不能证明精度没有丢失。对于只承担标识作用的 ID,参与算术没有意义,更不应该使用 Number 承载。

  • 安全整数上限:9007199254740991,即 Number.MAX_SAFE_INTEGER。
  • 数据库 bigint、Java long、Go int64 的取值范围可能明显大于 JavaScript 安全整数范围。
  • 银行卡号、手机号、订单号等标识符即使全是数字,也应按字符串建模。

三、首选方案:在接口契约中把大整数定义为字符串

最稳定的处理位置是数据生产端。服务端序列化时把可能超出安全范围的整数写成 JSON 字符串,OpenAPI 或 TypeScript 类型也同步定义为 string。这样标准 JSON.parse 就能无损得到原始十进制文本。

前端只有在确实需要整数运算时才调用 BigInt。若字段只是路由参数、缓存键或数据库主键,始终保留 string 更简单,也避免 BigInt 与 Number 混算带来的 TypeError。

type OrderResponse = {
  orderId: string;
  retryCount: number;
};

const raw = '{"orderId":"1819283748593829183","retryCount":2}';
const order = JSON.parse(raw) as OrderResponse;

console.log(order.orderId); // "1819283748593829183"
console.log(BigInt(order.orderId) + 1n); // 1819283748593829184n

四、后端暂时无法修改时,不要依赖普通 reviver 补救

JSON.parse 的 reviver 在解析过程完成后才处理属性。传统写法中的 value 已经是 Number,若此前发生舍入,再执行 BigInt(value) 只会把错误结果转换成 BigInt,原始末位无法恢复。

在无法调整接口的情况下,应先取得 response.text(),再交给能够保留数字原始词法的专用解析器,并明确配置为字符串或任意精度整数。不要用正则给“所有长数字”批量加引号,因为数字也可能出现在字符串、指数形式或嵌套文本中。

部分现代运行时会给基础类型 reviver 传入第三个 context 参数,其中 context.source 可读取该值在原始 JSON 中的文本。它可以按已知字段恢复 BigInt,但需要核对目标浏览器和 Node.js 版本,且仍不如服务端字符串契约稳定。

const raw = '{"id":9007199254740993}';

const wrong = JSON.parse(raw, (_key, value) =>
  typeof value === 'number' && !Number.isSafeInteger(value)
    ? BigInt(value)
    : value,
);

console.log(wrong.id); // 9007199254740992n,错误值已无法恢复

五、BigInt 回传接口时使用局部 replacer

原生 JSON.stringify 不会默认序列化 BigInt。更可控的做法是在当前序列化调用中使用 replacer,把 BigInt 转为十进制字符串,而不是全局修改 BigInt.prototype。

转成字符串后,接收端也必须按照字符串读取;如果后端仍把它强制转换成浮点数,精度问题只会移动到下一层。

function stringifyWithBigInt(value: unknown): string {
  return JSON.stringify(value, (_key, current) =>
    typeof current === 'bigint' ? current.toString() : current,
  );
}

const body = stringifyWithBigInt({
  orderId: 1819283748593829183n,
  action: 'confirm',
});

console.log(body);
// {"orderId":"1819283748593829183","action":"confirm"}

六、常见错误做法与适用边界

“超过 15 或 16 位就一定有问题”只能作为告警规则,不能替代 Number.isSafeInteger。安全性取决于具体数值,而不是十进制位数。

金额问题不能简单地统一改成 BigInt。BigInt 只表示整数,不支持小数;金额通常应使用最小货币单位整数、十进制定点库或服务端 decimal,并明确舍入规则。

把所有数字都改成字符串也并非必要。页码、数量、状态码等明确位于安全范围内且需要计算的字段继续使用 number 更合适。

  • 不要先 JSON.parse,再把不安全 Number 转成 BigInt。
  • 不要通过 parseInt、Math.round 或 toFixed 试图恢复已经丢失的末位。
  • 不要让同一字段在不同接口中一会儿返回 number、一会儿返回 string。
  • BigInt 不能与 Number 直接进行加减乘除,转换前要确认不会丢失信息。

七、如何验证接口已经修复

至少覆盖安全边界两侧、负数以及真实业务 ID。验证时同时断言类型和值,避免只比较控制台格式化后的显示结果。

若使用字符串契约,前端测试应确认原始数字逐字符保持一致;若需要 BigInt 计算,再单独测试从十进制字符串到 BigInt 的转换。

const samples = [
  '9007199254740991',
  '9007199254740992',
  '9007199254740993',
  '-9007199254740993',
  '1819283748593829183',
];

for (const id of samples) {
  const parsed = JSON.parse(`{"id":"${id}"}`) as { id: string };
  console.assert(parsed.id === id, `ID changed: ${id}`);
  console.assert(BigInt(parsed.id).toString() === id, `BigInt failed: ${id}`);
}

八、FAQ 与结论

问:JSON 规范为什么不直接限制为 53 位整数?答:JSON 是跨语言的数据格式,解析器可以使用不同的数值实现;互操作时需要双方约定可安全处理的范围。

问:BigInt 可以直接放进 JSON 吗?答:不可以直接 JSON.stringify;通常先转成十进制字符串,并在契约中标注类型。

问:只在前端把字段类型写成 string 能解决吗?答:不能。服务端响应文本中必须带双引号;如果数字先被 JSON.parse 成 Number,TypeScript 类型声明不会改变运行时值。

结论:大整数精度丢失的根因是 JavaScript Number 的安全整数边界。标识符优先采用字符串契约;需要整数运算时再从字符串显式转换为 BigInt,并用 replacer 回传。可继续阅读 /articles/json-unexpected-token 排查解析失败,或查看 /articles/nested-json-escaping 处理字符串化 JSON。

来源与延伸阅读

技术审校所依据的规范与权威参考资料。

相关文章

继续阅读

可打开关联的浏览器工具,使用自己的样本验证文中的处理流程。

打开关联工具