开发者工具 · JSON 格式化程序和验证程序
JSON 中的大整数 ID:为什么 JavaScript 格式化程序可能会对它们进行四舍五入
· 为什么它很重要
json 开发人员工作流程 验证
JSON 允许任何大小的整数,但 JavaScript 将数字表示为 64 位浮点数,因此任何高于 2^53 的内容在解析和重新序列化时都可能发生变化。这篇文章解释了限制、如何发现损坏以及如何保护 ID。
改了 1 的 ID
更改了 1 的 ID 通常会作为完全有效的 JSON 到达。将 `{"orderId":9007199254740993}` 放入 JavaScript 中,`JSON.parse` 返回一个数字,其显示值为 `9007199254740992`。解析成功,因为令牌遵循 JSON 数字语法;当将这些十进制数字转换为 JavaScript 的数字表示形式时,就会发生损坏。序列化解析值的格式化程序会忠实地写入四舍五入的数字,而不是源中出现的确切标记。
当引用相同的数字时,对比是立即的。 `JSON.parse("{"orderId":"9007199254740993"}")` 返回字符串 `9007199254740993`,保留每个字符,而 `JSON.stringify` 在引号内原封不动地发出这些数字。这就是为什么单独的语法验证无法保护数字标识符。每当出现长整数时就比较输入和输出,并且当算术不是其含义的一部分时,将标识符视为生成边界处的字符串。
RFC 8259 关于数字的说法
RFC 8259 定义了 JSON 数字的拼写,但并未为每个实现提供任意精度的数字类型。该语法允许可选的减号、整数部分以及可选的分数和指数部分。它不包括十六进制表示法、`NaN` 和 `Infinity` 等方便的符号。因此,即使常见的 JavaScript 使用者无法将该整数精确地表示为数字,`9007199254740993` 在语法上也是有效的。
该规范的互操作性指导是实际警告:软件通常使用 IEEE 754 二进制 64 数字,并且从负 `2^53 + 1` 到正 `2^53 - 1` 范围内的整数在精确一致的意义上是可互操作的。验证器可以正确接受较大的标记,而解析器稍后会对其进行舍入。
2^53 来自哪里
`2^53` 边界来自二进制 64 尾数中可用的精度。 JavaScript 将可连续表示的最高整数公开为 `Number.MAX_SAFE_INTEGER`,即 `9007199254740991`。在该大小或低于该大小时,可以清楚地表示相邻整数。在其上方,可表示值之间的间距增大,因此一些相邻的十进制整数映射到相同的数字。运行时不会截断字符串;它正在选择该有限二进制格式中可用的最接近的值。
一个揭示性的控制台检查是 `Number.isSafeInteger(9007199254740993)`,它是错误的,尽管源文字在函数接收它之前已经被舍入。另一个是 `9007199254740992 === 9007199254740993`,它在 JavaScript 中评估为 true。这些示例涉及精确的整数标识,而不是每个较大的数字是否变得不可用。
解析和重新序列化如何丢失数字
解析并重新序列化格式化分为三个阶段:读取数字字符,创建内存中值,然后从该值生成新字符。词汇细节在中间阶段消失。使用 `{"ticket":9223372036854775807}`,`JSON.parse` 创建最接近的可用 JavaScript 编号; `JSON.stringify` 然后发出 `9223372036854776000`。序列化器不会独立地破坏保留的令牌。到序列化时,原始数字序列不再存在于解析的对象中。
ToolAcre 的存储库实现使用 `JSON.parse` 和 `JSON.stringify`,因此此限制适用于其格式化输出。其语法扫描器运行以在解析失败后提供稳定的原因和位置;它不会用任意精度的表示形式替换 JavaScript 数字。因此,成功的验证结果可以建立语法,而格式差异可以揭示精度损失。
工作示例:比较输入和输出
比较 JavaScript 往返之前和之后的 `{"numeric":9007199254740993,"text":"9007199254740993"}`。运行 `JSON.stringify(JSON.parse(source), null, 2)` 会生成一个格式化对象,其 `numeric` 成员为 `9007199254740992`,而 `text` 仍为 `"9007199254740993"`。两个成员在输入中均有效,并且在输出中仍然有效。只有带引号的表示形式才能准确保留标识符,因为它被解码为字符数据而不是数字。
有用的评论不仅仅询问格式化程序是否显示绿色。在源中搜索不间断的数字序列,比较任何超出安全范围的值,并确定每个字段是否代表数量或不透明标签。如果生产者控制合同,请将标签更改为字符串,并为消费者记录该选择。
从源头保护 ID
通过在架构中将 ID 定义为字符串并在任何 JavaScript 客户端接收有效负载之前将其序列化为字符串,从源头保护 ID。 ID 可能只包含数字,但在含义上仍然是非数字:加法、舍入和按大小排序不是帐户密钥上的合法操作。字符串还保留前导零,即使其大小在安全范围内,数字表示也会丢弃这些前导零。
不要根据另一个运行时可以保存更大整数的事实来推断跨语言安全性。解析器和目标类型各不相同,并且用 JavaScript 编写的中介可以在后续服务看到该值之前对其进行舍入。一些专门的解析器保留数字标记或构造大整数,但每个参与者都必须共享该合约。
这不包括什么
这不包括更广泛的十进制算术设计。诸如 `0.1` 之类的值有自己的二进制浮点行为,并且根据应用程序合约,货币可能需要缩放整数或小数类型。引用每个数字也不会自动改进模式。计数、坐标和测量值通常是合法的数字。该决定取决于精确的十进制拼写或精确的整数标识是否必须保留在数据路径中的每个消费者中。
此讨论也没有声称 JSON 本身舍入了标记或所有解析器的行为都类似于 JavaScript。具体的存储库证据更窄:此格式化程序调用 `JSON.parse` 和 `JSON.stringify`,因此 JavaScript 数字语义控制此处未加引号的值。任意精度 JSON 库可以做出不同的选择,但它必须定义如何公开和序列化值。
要点:2^53 以上的数字属于字符串
要点是具体的:当 JavaScript 安全范围之外的整数标识符必须在不改变的情况下通过 JavaScript 时,它们就属于字符串。 `9007199254740993` 作为 JSON 数字是有效语法,但在 `JSON.parse` 之后变为 `9007199254740992`; `"9007199254740993"` 保持准确。引号不是装饰。他们选择一种将数字保留为数据的表示形式,并防止消费者将不透明的标签视为近似数量。
在用格式化程序输出替换文档之前,将长数字与原始数字进行比较并调查每个更改的数字。尽可能修复生产者和模式,以便所有下游客户端一致地接收安全表单。 ToolAcre 可以公开结果,因为它的输出反映了解析后的 JavaScript 值,但它无法重建在解析过程中已经丢失的数字。