JSON 格式化的三个易错点:大整数、重复键与错误定位

举报
yd_278092557 发表于 2026/10/20 19:35:33 2026/10/20
【摘要】 用实测代码解释 JSON.parse 与 JSON.stringify 可能改变大整数、数字写法和重复键的原因,介绍保留原始 token 的格式化方法、错误位置坐标规则及浏览器端工具的能力边界。

整理接口返回值时,最顺手的写法通常是 JSON.stringify(JSON.parse(source), null, 2)。对普通业务对象,这样做很方便;但如果工具的目标是“只调整排版,保留输入内容”,解析后重新序列化就可能带来意料之外的变化。

下面结合一个浏览器端 JSON 工具的实现,说明三个容易忽略的问题。文中的运行结果使用 Node.js v24.14.1 验证,核心代码与测试可在文末的公开仓库中查看。

一、大整数没有报错,内容却变了

先看一段符合 JSON 数字语法的输入:

const source = '{"id":900719925474099312345,"amount":1.2500,"tiny":1e-9999,"big":1e9999,"zero":-0,"word":"\\u4f60\\u597d"}';

console.log(JSON.stringify(JSON.parse(source)));

实际输出:

{"id":900719925474099300000,"amount":1.25,"tiny":0,"big":null,"zero":0,"word":"你好"}

这里同时发生了几种变化:

  • id 超过 JavaScript 安全整数范围,转成 Number 时已经丢失精度。
  • 1.2500 被重新写成 1.25;如果要保留输入的小数位写法,这个信息已经消失。
  • 1e-9999 在此运行环境中下溢为 0;1e9999 转为 Infinity,随后被 JSON.stringify 序列化为 null。
  • -0 被输出成 0。
  • Unicode 转义被解码后重新输出成汉字,字符串值相同,文本写法不同。

这些变化中,既有数值精度或范围变化,也有数值相等但表示形式变化。对于日志排查、接口签名材料核对、订单编号检查,这种区别很关键。

RFC 8259 没有把 JSON 数字限制为 JavaScript 的 Number,但接收端可以有自己的范围与精度限制。跨系统可靠交换整数时,常用的精确互操作范围是 -(2**53)+1 到 (2**53)-1。业务中的长编号可以在接口约定中直接使用字符串;格式化工具则应尽量保留用户原本输入的数字文本。

二、重复键应提示,也应保留证据

再看这个输入:

const source = '{"a":1,"\\u0061":2,"__proto__":3}';
console.log(JSON.stringify(JSON.parse(source)));
// {"a":2,"__proto__":3}

"a" 和 "\u0061" 解码后是同一个属性名。在 JavaScript 的普通解析结果中,前一个成员被后一个覆盖,重新输出时已经无法看到重复出现过。

RFC 8259 对对象成员名的要求是 SHOULD 唯一。不能把所有重复键一概说成 JSON 语法错误;不同接收端可能保留最后一个、拒绝输入,或者保留全部成员。因此,排查工具更有用的做法是提示风险,同时保留原始成员和顺序。

重复检测还需要关注两个细节:

  1. 比较解码后的键名,才能发现 "a" 与 "\u0061" 的重复。
  2. 每个对象分别维护键集合,避免把不同层级中同名的字段误报为重复。

实现中可以为每个对象创建 Set。读取一个属性名后,只解码这个字符串,用于判重;展示和输出仍使用包含双引号与转义的原始 token。Set 也能正常处理 __proto__ 这样的字符串键名。

对于示例输入,工具会提示 1 次重复,压缩输出仍保留全部三个成员:

{"a":1,"\u0061":2,"__proto__":3}

这能保留问题现场,但不会替用户决定最终应该采用哪个值。接收接口需要唯一键时,仍应修正数据来源。

三、先校验语法,再围绕原始 token 调整空白

为保留原始表示,可以把处理过程分成两步:先校验 JSON 语法并收集原始 token,再根据 {、[、,、: 等结构符号组织缩进。

数字识别的关键是记录原文,而不是马上调用 Number():

const number = source.slice(pos).match(
  /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/
);

if (number) {
  pos += number[0].length;
  tokens.push(number[0]);
}

这只是完整解析器中的数字分支。对象、数组、字符串、转义、分隔符和输入结束位置仍需逐项校验。例如 01 虽然可以先匹配到 0,完整校验还会拒绝后面多余的 1,不能因为正则匹配成功就接受整份输入。

字符串也保留从开始双引号到结束双引号的原始片段。扫描器在字符串内部处理反斜杠与转义序列,因此字符串中的逗号、括号不会被误当作结构分隔符。这也是不能简单用正则替换逗号来添加换行的原因。

仓库中的核心接口如下:

import { parseJson, formatTokens } from './json-core.mjs';

const source = '{"id":900719925474099312345,"amount":1.2500}';
const parsed = parseJson(source);

console.log(formatTokens(parsed.tokens));
console.log(parsed.tokens.join(''));

格式化结果:

{
  "id": 900719925474099312345,
  "amount": 1.2500
}

压缩结果:

{"id":900719925474099312345,"amount":1.2500}

结构之外的空白可以重新组织,数字写法、字符串转义、重复成员与成员顺序都来自原始 token。这里保留的是文本表示,并没有提供任意精度数值运算能力。

现代 JavaScript 引擎还可以通过 JSON.parse 的 reviver 第三个参数中的 context.source 取得部分原始值的源码,具体需核对目标运行环境的支持情况。普通两参数 reviver 执行时,数字已完成转换;而要完整保留重复对象成员和整体词法表示,仍需选择适合该目标的解析结构。

四、错误位置要说明坐标规则

只显示“JSON 格式错误”,用户通常需要重新检查整段内容。更实用的错误信息应包括原因、行号、列号和原文偏移。

以末尾多余逗号为例:

parseJson('{\n "a": 1,\n}');
// JsonInputError
// message: JSON 不允许末尾多余的逗号。
// position: 11
// line: 3
// column: 1

这个实现用 JavaScript 字符串索引记录偏移,position 从 0 开始,行列从 1 开始;换行以 \n 统计。列号按 UTF-16 代码单元计算,不等于屏幕上的字形宽度或用户感知字符数。例如部分 emoji 占两个代码单元,制表符的显示宽度也取决于编辑器。

前端高亮或跳转时,必须和编辑器约定同一种坐标规则,否则提示位置正确,光标却可能落在旁边。

五、用回归样例明确能力边界

这个工具的测试覆盖了普通结构、大整数与指数写法、重复键、非法语法、错误位置、输入限制与缩进。实际运行的 6 组测试均通过。对于格式化器,最重要的验收条件之一是:格式化后再次校验,得到的 token 序列应与原输入一致。

当前实现将输入限制为 UTF-8 编码后的 1 MiB,并设置 200 层的嵌套深度保护。它会把完整 token 数组保存在内存中,适合浏览器中的日常接口数据整理;超大文件需要另行设计流式处理。它也只校验 JSON 语法,不负责 JSON Schema、字段类型约定或业务有效性检查。

选择工具时,可以用本文中的大整数、重复键和多余逗号三个样例做一次快速检查。能否保留原始信息、能否指出具体错误,往往比缩进样式有多少种更影响排查效率。

代码与参考资料

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。