JSON 工具箱

同一份 JSON,四个工具给出四种结果:格式化工具怎么选

结论:格式化不等于无损:带前导零、BOM 或大整数的同一份 JSON,经 Python、Node、jq、json.tool 处理后有的报错、有的静默改值。10 份样本实测四条解析路径,给出按场景选型的对照表与一段可复制的严格校验脚本。

进阶约 6 分钟发布于 2026-11-03更新于 2026-11-03
四条解析路径对同一份 JSON 给出不同结果的封面示意

结论:格式化工具之间最要命的差别不是缩进几个空格,而是同一份输入,有的报错、有的悄悄把数据改了。选工具前先拿一段带边界值的样本跑一遍,比看功能列表有用得多。

用户以为格式化只是「重新排版」,实际上每条路径都在用自己的解析器重新解释这份文本。解析器一宽松,本该抛出的报错就变成了静默改数据,而页面上依然是一片绿色的「格式化成功」。

一、现象:同一份输入,四条路径给出四种结果

实测环境是 Python 3.11.1、Node v22.13.1、jq 1.7,样本为 10 段手工构造的短 JSON,全部经标准输入送入、不落盘、不改配置。四条路径分别是 json.loads、JSON.parse、jq -c .、python -m json.tool。表中 ERR 表示抛异常并中断,静默→ 表示不报错但输出值与输入已经不一致。

样本 python json.loads node JSON.parse jq -c . python -m json.tool
S1 {"a":1,"b":[1,2]} OK OK OK OK
S2 {"a":1,} ERR ERR ERR ERR
S3 {'a':1} ERR ERR ERR ERR
S4 {"a":01} ERR char 6 ERR position 6 静默→ {"a":1} ERR
S5 {"a":1 /*x*/} ERR char 7 ERR ERR ERR
S6 \ufeff{"a":1} ERR utf-8-sig ERR position 0 静默→ {"a":1} ERR
S7 {"id":9007199254740993} OK 保真 静默→ 9007199254740992 OK 保真 OK
S8 {"a":1,"a":2} 静默→ {"a":2} 静默→ {"a":2} 静默→ {"a":2} OK
S9 {"a":1 ERR char 6 ERR ERR EOF ERR
S10 <html>502</html> ERR char 0 ERR Unexpected token '<' ERR ERR

十个样本里有四个出现「不报错但数据变了」:S4、S6、S7、S8。也就是说,只看「格式化成功」这盏绿灯,有四成概率你拿到的已经不是原来那份数据。真正让人踩坑的是这几条都不在语法错误列表里——文件能打开、能复制、能导出,错只在内容。

二、原因:语法层和语义层根本是两件事

RFC 8259 第 1.2 节说得很直白:ECMA-404 允许若干实践,而 RFC 为了最大互操作性建议避免;第 4 节要求对象名 SHOULD 唯一,却没有规定重复键怎么处理;第 6 节讨论数字时只给出互操作建议。ECMA-404 更进一步,明确写「对数字的语义不可知」,只规定数字序列的表示,不规定语言内部怎么存。换句话说,规范把大量边界留给了实现者。

于是「格式化」这一步就分化成两层:语法层判断文本是否合法,语义层决定这段文本变成什么值。前者有标准答案,后者没有。jq 把 01 读成 1、把开头的 BOM 当空白跳过,是它在语义层的选择;Node 把 20 位整数塞进 IEEE 754 双精度,是 JavaScript 运行时的选择;四条路径对 {"a":1,"a":2} 都取最后一个值,是主流解析器的共同默契。这些都不违反「能解析」,但都改变了数据。

JSON 解析报错怎么修:Unexpected token 逐条排查 处理的是语法层,把报错原文翻译成坐标;本文处理的是它前面那一步——如果工具压根没报错,你连坐标都拿不到。

三、四类静默改数据,逐条看它们改了什么

把四个「静默」样本单独拎出来,改动都很小,小到肉眼扫一遍不会发现:

{"a": 01}                          // 前导零:jq 输出 {"a":1}
\ufeff{"a": 1}                     // BOM:jq 直接跳过开头三字节
{"id": 9007199254740993}           // 20 位整数:JSON.parse 输出 ...992
{"a": 1, "a": 2}                   // 重复键:四条路径都只留 {"a":2}

前导零和 BOM 属于「输入本来就不合规,但工具替你宽容了」。01 在标准 JSON 里非法(RFC 8259 第 6 节禁止前导零),BOM 也不该出现在 JSON 文本开头;Python 与 Node 都明确报错,jq 却选择继续。这类宽容在日志清洗时很方便,在契约校验时是灾难:上游以为自己发的是非法数据需要修,下游已经拿到一份「看起来正常」的结果。

大整数属于「输入完全合规,是运行时装不下」。9007199254740993 是合法 JSON 数字,但超过 2^53-1,JSON.parse 只能表示成 9007199254740992。Python 的 int 与 jq 都保住了原值。如果这份 JSON 里是订单号或雪花 ID,末位被改一位就意味着对不上账。

重复键属于「规范没规定,实现各自选」。四条路径全部静默取后者,没有任何一条给出警告。RFC 8259 只说对象名 SHOULD 唯一,所以这不是 bug,但它是拼接配置、合并补丁时最难查的一类问题:《从 RFC 8259 读懂 JSON 语法边界》对这类「允许但不建议」有更细的展开。

四、报错定位能力:谁给行列,谁只给一句

再看出错的六个样本,四条路径给出的信息量差别很大。Python 的 JSONDecodeError 同时给 msg、lineno、colno、pos;Node v22 的报错带 position N (line 1 column M) 并附上出错处的原文片段;python -m json.tool 复用 Python 的信息;jq 只给一句 parse error: ... at line N,不给字符偏移。

单行压缩 JSON 上,行列号基本失效——所有内容都在 line 1。这时候要的是字符偏移,下面这段脚本把校验统一成「报错 + 偏移 + 上下文」:

import json, sys

def strict_check(text: str) -> None:
    try:
        json.loads(text)
        print("OK")
    except json.JSONDecodeError as e:
        print(f"ERR {e.msg} | char {e.pos} | line {e.lineno} col {e.colno}")
        lo, hi = max(0, e.pos - 30), min(len(text), e.pos + 30)
        print("...", text[lo:hi].replace("\n", "\\n"), "...")
        sys.exit(1)

strict_check(open(sys.argv[1], encoding="utf-8").read())

它比 jq . 严格(会拒绝前导零与 BOM),而且给出 e.pos。具体怎么把 line 1 column 23611 换算成字节位置、中文文档里字符偏移和字节偏移为什么会差三倍,单行 JSON 报错怎么定位:从 line 1 column 23611 找到出错那个字节 已经讲透,这里不重复。

五、怎么改:把格式化拆成「严格校验」+「美化」两步

结论很简单:不要用一个「格式化」按钮同时承担校验与美化。先用严格解析器判合法性,再决定用什么工具排版。一行命令就能把第一步独立出来:

python3 -m json.tool input.json > /dev/null && echo "语法 OK" || echo "语法不合法"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json

第一步只回答「能不能解析」,第二步再检查「解析出来是不是你期望的结构」。若数据里有大整数,改用 Python 或 jq 读取,别走 JSON.parse;若源数据可能带 BOM,读入时显式指定 encoding='utf-8-sig' 而不是指望工具自动跳过;若键可能重复,得在解析前单独扫一遍,因为四条路径都不会提醒你。把这几条写进 CI,就是 编辑器、命令行与自动化工具链 讲的流水线,只是多了一层数据保真断言。

六、怎么选:四条路径的选型表

路径 强项 风险 适合
python json.loads / json.tool 严格、给行列与字符偏移、整数不丢精度 拒绝前导零与 BOM,需显式处理编码 契约校验、CI、日志排查
node JSON.parse 浏览器原生、报错附带原文片段 大整数静默截断、BOM 报错 前端调试、浏览器内工具
jq -c . 管道友好、最快出结果 前导零与 BOM 静默通过、不给字符偏移 运维管道、快速取字段
浏览器格式化工具 /parse、/editor 树形浏览、键排序、复制路径、无需装环境 行为取决于网页用的解析器,必须先验一次 人工查看、临时排障

选型的关键不是「哪个功能多」,而是「它对我的数据会不会撒谎」。带订单号、金额、设备 ID 的数据,优先 Python 或明确声明保真的工具;只想看结构、不落库的调试,浏览器工具最省事。本站 /parse 走的是浏览器内的语法校验与格式化,适合「先看清楚长什么样」,但涉及大整数时仍建议按上面的脚本复核一次。

七、隐私只是其中一条边界

「数据会不会上传」是选在线工具时最常被问的一条,但它和「数据会不会被改」是两件独立的事:本地运行、断网可用的工具,照样可能把 20 位整数截断;反过来,一个严格报错的在线工具,也可能把原文 POST 到服务器。两条要分别验证,不能互相替代。《在线 JSON 格式化会上传数据吗》那篇给了三个两分钟能验完的动作;本文补的是另一半——拿下面这份样本清单,确认工具不会静默改值。

八、一份可复制的自检清单

把 S1–S10 这十段存成一个 cases.txt,每行一段,粘进任何候选工具,核对三件事:报错的样本是否真的报错、报了错是否给到具体位置、没报错的样本输出是否与输入逐字一致。特别核对 S4、S6、S7、S8 四行——只要有一行「静默→」,就说明这条路径不能单独用来做数据校验。

再补三条工程约束:把严格校验放进提交前钩子,别等上线才发现大整数对不上账;多语言环境下统一用 UTF-8 无 BOM 写文件;对来自第三方的 JSON 保留原始响应体,格式化后的结果只用于阅读,不用于回写。《编辑器、命令行与自动化工具链》里那套 pre-commit 配置可以直接接上这份清单。

常见问题

格式化通过,是不是就说明 JSON 没问题?

不是。S4、S6、S7、S8 四个样本都「通过」了格式化,但前导零、BOM、20 位整数和重复键都被改写或吞掉。格式化只证明文本能被某个解析器接受,不证明解析后的值等于你发出的值;关键字段要单独做逐值断言。

为什么 jq 会接受 Python 报错的前导零和 BOM?

jq 有自己的词法分析器,对 01 这类数字采取宽松策略,也会跳过开头的 BOM;Python 与 Node 按 RFC 8259 严格拒绝。这不是版本 bug,是「容错解析器」与「严格解析器」的取舍。做清洗时前者省事,做契约校验时必须用后者。

大整数被改成 9007199254740992,怎么避免?

JavaScript 的 Number 是 IEEE 754 双精度,安全整数上限是 2^53-1。用 JSON.parse 读 20 位 ID 必然截断。改用字符串保存这类 ID,或改用 Python int、jq、以及支持 BigInt 的解析库;前端确实需要数值时,在解析前把该字段替换成字符串再处理。

在线工具和本地命令行,到底该用哪个?

按数据敏感度与用途分:只是看结构、找字段,浏览器工具最快;要进流水线、要断言数据保真,用 python -m json.tool 加严格校验脚本。两者都要先跑一遍 S1–S10 样本,确认报错与输出符合预期,再决定输入哪一类数据。