[{"data":1,"prerenderedAt":198},["ShallowReactive",2],{"article-json-number-precision":3},{"article":4,"relatedArticles":151},{"slug":5,"category":6,"date":7,"readingMinutes":8,"tags":9,"relatedTool":15,"cn":16,"en":79,"author":139,"reviewedBy":142,"reviewedDate":144,"updatedDate":7,"sources":145,"noIndex":150},"json-number-precision","踩坑避坑","2026-07-28",8,[10,11,12,13,14],"JSON","JavaScript","Number","BigInt","精度丢失","/tools/json/json-validator",{"title":17,"description":18,"intro":19,"sections":20},"JSON 大整数精度丢失：Number 安全范围与解决方案","说明 JSON 大整数进入 JavaScript 后为什么会被舍入，演示 Number.MAX_SAFE_INTEGER 边界，并给出字符串契约、BigInt 转换和序列化的可验证方案。","接口中的订单 ID、雪花 ID 或数据库 bigint 字段在原始响应里明明正确，经过 JSON.parse 后尾数却发生变化。问题不在 JSON 文本是否有效，而在解析结果被存入 JavaScript Number 时超出了安全整数范围。本文只讨论“大整数精度”这一问题，不讨论超大 JSON 的渲染性能或普通小数的财务计算。",[21,27,36,42,49,55,66,72],{"heading":22,"paragraphs":23,"code":26},"一、最小复现：解析成功不代表数值准确",[24,25],"下面的 JSON 语法完全合法，JSON.parse 也不会抛出异常，但 orderId 的解析结果已经与原始数字不同。最危险的地方正是这种错误通常没有报错，只会在展示、比较或回传 ID 时暴露。","不要只用控制台输出后的数字判断接口是否正确。应同时查看 Network 面板中的原始响应文本，并用 Number.isSafeInteger 检查解析后的整数。","const raw = '{\"orderId\":9007199254740993,\"count\":42}';\nconst data = JSON.parse(raw);\n\nconsole.log(data.orderId); // 9007199254740992\nconsole.log(Number.isSafeInteger(data.orderId)); // false\nconsole.log(data.count); // 42\nconsole.log(Number.isSafeInteger(data.count)); // true",{"heading":28,"paragraphs":29,"bullets":32},"二、根因：JSON 数字与 JavaScript Number 不是同一层限制",[30,31],"JSON 使用十进制数字字面量表示数值；接收端采用什么数值类型，由具体实现决定。JavaScript 的 Number 使用 IEEE 754 binary64，整数在 -(2^53 - 1) 到 2^53 - 1 之间可以连续、精确地表示，这个上限可通过 Number.MAX_SAFE_INTEGER 获取。","超过安全范围后，并不是所有整数都会立即变成 Infinity，而是相邻整数可能被舍入到同一个可表示值。因此“位数看起来还在”不能证明精度没有丢失。对于只承担标识作用的 ID，参与算术没有意义，更不应该使用 Number 承载。",[33,34,35],"安全整数上限：9007199254740991，即 Number.MAX_SAFE_INTEGER。","数据库 bigint、Java long、Go int64 的取值范围可能明显大于 JavaScript 安全整数范围。","银行卡号、手机号、订单号等标识符即使全是数字，也应按字符串建模。",{"heading":37,"paragraphs":38,"code":41},"三、首选方案：在接口契约中把大整数定义为字符串",[39,40],"最稳定的处理位置是数据生产端。服务端序列化时把可能超出安全范围的整数写成 JSON 字符串，OpenAPI 或 TypeScript 类型也同步定义为 string。这样标准 JSON.parse 就能无损得到原始十进制文本。","前端只有在确实需要整数运算时才调用 BigInt。若字段只是路由参数、缓存键或数据库主键，始终保留 string 更简单，也避免 BigInt 与 Number 混算带来的 TypeError。","type OrderResponse = {\n  orderId: string;\n  retryCount: number;\n};\n\nconst raw = '{\"orderId\":\"1819283748593829183\",\"retryCount\":2}';\nconst order = JSON.parse(raw) as OrderResponse;\n\nconsole.log(order.orderId); // \"1819283748593829183\"\nconsole.log(BigInt(order.orderId) + 1n); // 1819283748593829184n",{"heading":43,"paragraphs":44,"code":48},"四、后端暂时无法修改时，不要依赖普通 reviver 补救",[45,46,47],"JSON.parse 的 reviver 在解析过程完成后才处理属性。传统写法中的 value 已经是 Number，若此前发生舍入，再执行 BigInt(value) 只会把错误结果转换成 BigInt，原始末位无法恢复。","在无法调整接口的情况下，应先取得 response.text()，再交给能够保留数字原始词法的专用解析器，并明确配置为字符串或任意精度整数。不要用正则给“所有长数字”批量加引号，因为数字也可能出现在字符串、指数形式或嵌套文本中。","部分现代运行时会给基础类型 reviver 传入第三个 context 参数，其中 context.source 可读取该值在原始 JSON 中的文本。它可以按已知字段恢复 BigInt，但需要核对目标浏览器和 Node.js 版本，且仍不如服务端字符串契约稳定。","const raw = '{\"id\":9007199254740993}';\n\nconst wrong = JSON.parse(raw, (_key, value) =>\n  typeof value === 'number' && !Number.isSafeInteger(value)\n    ? BigInt(value)\n    : value,\n);\n\nconsole.log(wrong.id); // 9007199254740992n，错误值已无法恢复",{"heading":50,"paragraphs":51,"code":54},"五、BigInt 回传接口时使用局部 replacer",[52,53],"原生 JSON.stringify 不会默认序列化 BigInt。更可控的做法是在当前序列化调用中使用 replacer，把 BigInt 转为十进制字符串，而不是全局修改 BigInt.prototype。","转成字符串后，接收端也必须按照字符串读取；如果后端仍把它强制转换成浮点数，精度问题只会移动到下一层。","function stringifyWithBigInt(value: unknown): string {\n  return JSON.stringify(value, (_key, current) =>\n    typeof current === 'bigint' ? current.toString() : current,\n  );\n}\n\nconst body = stringifyWithBigInt({\n  orderId: 1819283748593829183n,\n  action: 'confirm',\n});\n\nconsole.log(body);\n// {\"orderId\":\"1819283748593829183\",\"action\":\"confirm\"}",{"heading":56,"paragraphs":57,"bullets":61},"六、常见错误做法与适用边界",[58,59,60],"“超过 15 或 16 位就一定有问题”只能作为告警规则，不能替代 Number.isSafeInteger。安全性取决于具体数值，而不是十进制位数。","金额问题不能简单地统一改成 BigInt。BigInt 只表示整数，不支持小数；金额通常应使用最小货币单位整数、十进制定点库或服务端 decimal，并明确舍入规则。","把所有数字都改成字符串也并非必要。页码、数量、状态码等明确位于安全范围内且需要计算的字段继续使用 number 更合适。",[62,63,64,65],"不要先 JSON.parse，再把不安全 Number 转成 BigInt。","不要通过 parseInt、Math.round 或 toFixed 试图恢复已经丢失的末位。","不要让同一字段在不同接口中一会儿返回 number、一会儿返回 string。","BigInt 不能与 Number 直接进行加减乘除，转换前要确认不会丢失信息。",{"heading":67,"paragraphs":68,"code":71},"七、如何验证接口已经修复",[69,70],"至少覆盖安全边界两侧、负数以及真实业务 ID。验证时同时断言类型和值，避免只比较控制台格式化后的显示结果。","若使用字符串契约，前端测试应确认原始数字逐字符保持一致；若需要 BigInt 计算，再单独测试从十进制字符串到 BigInt 的转换。","const samples = [\n  '9007199254740991',\n  '9007199254740992',\n  '9007199254740993',\n  '-9007199254740993',\n  '1819283748593829183',\n];\n\nfor (const id of samples) {\n  const parsed = JSON.parse(`{\"id\":\"${id}\"}`) as { id: string };\n  console.assert(parsed.id === id, `ID changed: ${id}`);\n  console.assert(BigInt(parsed.id).toString() === id, `BigInt failed: ${id}`);\n}",{"heading":73,"paragraphs":74},"八、FAQ 与结论",[75,76,77,78],"问：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。",{"title":80,"description":81,"intro":82,"sections":83},"Large Integers in JSON: JavaScript Precision Limits and Fixes","Learn why valid JSON integers can be rounded when parsed into JavaScript Number values, and apply string contracts, BigInt conversion, and safe serialization without losing digits.","An order ID or database bigint can be correct in the raw HTTP response but change after JSON.parse. The JSON text is valid; the loss happens when the parsed value is stored as a JavaScript Number outside the safe-integer range. This article focuses only on integer precision, not large-file rendering or decimal-money arithmetic.",[84,90,99,105,112,117,127,132],{"heading":85,"paragraphs":86,"code":89},"1. Minimal reproduction: parsing can succeed with the wrong value",[87,88],"The following payload is valid JSON and JSON.parse does not throw, yet the parsed orderId no longer matches the source text. Silent rounding is more dangerous than a syntax error because it can survive until the ID is displayed, compared, or sent back to an API.","Inspect both the raw Network response and the in-memory value. Number.isSafeInteger provides a direct boundary check for integer Number values.","const raw = '{\"orderId\":9007199254740993,\"count\":42}';\nconst data = JSON.parse(raw);\n\nconsole.log(data.orderId); // 9007199254740992\nconsole.log(Number.isSafeInteger(data.orderId)); // false\nconsole.log(Number.isSafeInteger(data.count)); // true",{"heading":91,"paragraphs":92,"bullets":95},"2. Root cause: JSON number syntax is not a JavaScript storage guarantee",[93,94],"JSON represents numbers as decimal literals, while each implementation chooses how to store them. JavaScript Number uses IEEE 754 binary64. Every integer from -(2^53 - 1) through 2^53 - 1 is exactly representable, exposed as Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER.","Beyond that range, adjacent integers can round to the same representable value. An identifier made only of digits is still an identifier; it should not become a Number merely because its wire representation looks numeric.",[96,97,98],"Number.MAX_SAFE_INTEGER is 9007199254740991.","Database bigint, Java long, and Go int64 ranges can exceed JavaScript safe integers.","Order IDs, account numbers, and trace IDs should normally be modeled as strings.",{"heading":100,"paragraphs":101,"code":104},"3. Preferred fix: define large integers as strings in the API contract",[102,103],"Serialize potentially large integers as JSON strings at the data producer and declare them as string in OpenAPI and TypeScript. Standard JSON.parse then preserves every decimal digit.","Convert to BigInt only when integer arithmetic is required. For routing, cache keys, and database identifiers, keeping the value as a string is simpler and avoids accidental Number/BigInt mixing.","type OrderResponse = {\n  orderId: string;\n  retryCount: number;\n};\n\nconst raw = '{\"orderId\":\"1819283748593829183\",\"retryCount\":2}';\nconst order = JSON.parse(raw) as OrderResponse;\n\nconsole.log(order.orderId);\nconsole.log(BigInt(order.orderId) + 1n);",{"heading":106,"paragraphs":107,"code":111},"4. A normal reviver cannot generally recover lost digits",[108,109,110],"A traditional JSON.parse reviver receives values after numeric conversion. Turning an already-rounded Number into BigInt only preserves the rounded value. When the server cannot change, obtain response.text() and use a parser designed to retain numeric source text or arbitrary-precision values.","Avoid regex-based rewrites that quote every long digit sequence. They can corrupt numbers inside strings, exponents, escaped content, or unrelated fields.","Some modern runtimes pass a third context argument for primitive reviver calls; context.source exposes the original JSON token. It can reconstruct a known field as BigInt, but compatibility must be verified and a string API contract remains the more portable design.","const raw = '{\"id\":9007199254740993}';\nconst wrong = JSON.parse(raw, (_key, value) =>\n  typeof value === 'number' && !Number.isSafeInteger(value)\n    ? BigInt(value)\n    : value,\n);\n\nconsole.log(wrong.id); // 9007199254740992n",{"heading":113,"paragraphs":114,"code":116},"5. Serialize BigInt with a local replacer",[115],"JSON.stringify does not serialize BigInt by default. A local replacer is explicit and avoids changing global built-in behavior. The receiving API must also treat the resulting decimal text as a string.","function stringifyWithBigInt(value: unknown): string {\n  return JSON.stringify(value, (_key, current) =>\n    typeof current === 'bigint' ? current.toString() : current,\n  );\n}\n\nconsole.log(stringifyWithBigInt({ id: 1819283748593829183n }));\n// {\"id\":\"1819283748593829183\"}",{"heading":118,"paragraphs":119,"bullets":122},"6. Incorrect fixes and boundaries",[120,121],"Digit count is only a warning heuristic; Number.isSafeInteger checks the actual value. parseInt, Math.round, and toFixed cannot reconstruct digits that were already rounded.","BigInt is not a universal decimal-money solution because it stores integers only. Use minor-unit integers with explicit scale or a decimal type when fractional values and rounding rules matter.",[123,124,125,126],"Do not parse first and convert an unsafe Number to BigInt later.","Do not return the same field as number in one endpoint and string in another.","Do not mix Number and BigInt arithmetic without an explicit, safe conversion.","Keep ordinary counters and page numbers as Number when their range is well defined.",{"heading":128,"paragraphs":129,"code":131},"7. Verification checklist",[130],"Test values on both sides of the safe boundary, negative values, and representative production-shaped IDs. Assert both the runtime type and the exact decimal text.","const samples = [\n  '9007199254740991',\n  '9007199254740992',\n  '9007199254740993',\n  '-9007199254740993',\n];\n\nfor (const id of samples) {\n  const parsed = JSON.parse(`{\"id\":\"${id}\"}`) as { id: string };\n  console.assert(parsed.id === id);\n  console.assert(BigInt(parsed.id).toString() === id);\n}",{"heading":133,"paragraphs":134},"8. FAQ and conclusion",[135,136,137,138],"Can JSON itself carry integers larger than 53 bits? Yes, but interoperability depends on the receiver’s numeric implementation.","Can BigInt be placed directly in JSON? Not through default JSON.stringify; encode it as decimal text with an explicit policy.","Does a TypeScript string annotation fix a numeric payload? No. Runtime JSON must contain quotes around the value.","The durable fix is a string contract for identifiers, optional BigInt conversion for arithmetic, and explicit serialization when sending BigInt back. See /articles/json-unexpected-token for syntax failures and /articles/nested-json-escaping for embedded JSON strings.",{"name":140,"url":141},"Tiny's Tool Editorial Team","https://tinystool.org/about",{"name":143,"url":141},"Tiny's Tool Technical Review","2026-08-08",[146],{"title":147,"url":148,"publisher":149},"RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format","https://www.rfc-editor.org/rfc/rfc8259","RFC Editor",false,[152,167,183],{"slug":153,"noIndex":150,"category":154,"date":7,"updatedDate":7,"reviewedDate":144,"readingMinutes":155,"tags":156,"relatedTool":15,"cn":161,"en":164},"json-unexpected-token","错误排查",10,[10,157,158,159,160],"JSON.parse","SyntaxError","Unexpected token","调试",{"title":162,"description":163},"JSON Unexpected token：8 类原因与定位","从原始响应、错误位置和 JSON 语法三层定位 JSON.parse 的 Unexpected token，覆盖 HTML 响应、重复解析、尾逗号、引号、控制字符、非法数字和截断数据。",{"title":165,"description":166},"JSON.parse Unexpected Token: 8 Causes and a Reliable Debugging Flow","Debug JSON.parse syntax failures by inspecting the raw response, input type, error position, and JSON grammar. Covers HTML responses, double parsing, trailing commas, escaping, invalid numbers, and truncated payloads.",{"slug":168,"noIndex":169,"category":6,"date":7,"updatedDate":7,"reviewedDate":144,"readingMinutes":170,"tags":171,"relatedTool":176,"cn":177,"en":180},"json-to-sql-injection-quotes",true,6,[172,173,174,175],"JSON to SQL","PostgreSQL","Parameterized Query","SQL Literal","/tools/json/json-to-sql",{"title":178,"description":179},"JSON 转 SQL：PostgreSQL 参数化查询与字面量转义边界","以 PostgreSQL 为例区分参数化写库与离线 SQL 导出，解释单引号、NULL、布尔值和数值的处理，避免把字符串替换误当成通用注入防护。",{"title":181,"description":182},"JSON to SQL: PostgreSQL Parameters and Literal-Escaping Limits","Using PostgreSQL, distinguish parameterized writes from offline SQL export and handle quotes, NULL, booleans, and finite numbers without treating string replacement as injection protection.",{"slug":184,"noIndex":169,"category":6,"date":7,"updatedDate":7,"reviewedDate":144,"readingMinutes":185,"tags":186,"relatedTool":191,"cn":192,"en":195},"json-to-ts-null-optional-union-types",4,[187,188,189,190],"JSON to TypeScript","Null","Optional","Union Types","/tools/json/json-to-typescript",{"title":193,"description":194},"JSON 转 TypeScript：null、字段缺失、optional 与 union","区分 JSON 中字段缺失与 null，说明 TypeScript optional、strictNullChecks 和 union 类型在接口建模中的不同含义。",{"title":196,"description":197},"JSON to TypeScript: null, Missing Fields, Optional, and Union Types","Distinguish missing JSON properties from null and model optional, nullable, and union states with strict TypeScript checks.",1786725508061]