JSON 格式化器(formatter / beautifier / pretty printer / validator)不是装饰器,是 API 调试、配置文件管理、数据交换、JWT payload 解码的可读性基础设施。大多数开发者的格式化习惯是「在 IDE 里按一下格式化快捷键」,这能解决 30% 的问题,剩下的 70%(trailing comma、Unicode 转义、大整数精度丢失、BOM 字符、单引号、NaN / Infinity 序列化、排序键副作用)是 IDE 解决不了的。

JSON 格式化器的核心价值不是「让 JSON 看起来好看」,是让数据在 API 调试 / 配置 review / 日志排查 / JWT 解码时被人眼读得懂。所以它属于基础设施,不是美化器。本文 8 个真实踩坑,每个「症状 + 为什么 + 怎么修」三段式讲清楚,文末给 Piick JSON 格式化工具,语法高亮 + 排序键 + 容错解析(JSON5 / 单引号 / 注释)+ 错误位置点击跳转 + 5 项统计(键 / 数组 / 最大嵌套深度 / 字节),1 分钟走完。

30 秒总览

  • JSON 格式化 ≠ 美化:前者是 API / 配置 / 日志的可读性基础设施,后者只是「让 JSON 看起来整齐」
  • JSON 标准的 4 个硬约束:不允许 trailing comma、字符串必须双引号、不允许注释、不允许 NaN / Infinity / undefined(4 个工具开发者必踩)
  • JS 生态的 3 个常见误用:trailing comma(ES2017+ 合法)、单引号对象(JS 字面量)、模板字符串嵌套 JSON(转义灾难)
  • 8 个常见 foot-guns:trailing comma / Unicode 转义 / 大整数精度 / 缩进 ≠ 嵌套 / BOM 字符 / 单引号 / NaN-Infinity-undefined 序列化 / 排序键副作用
  • 修复方向:永远用工具校验(尤其是生产 API 配置)、永远容错解析(开发期)、永远保留大整数为字符串(Snowflake ID)、永远不自动排序键(影响 Git diff)
  • 用 Piick JSON 格式化工具在线格式化 / 校验 / 美化,容错解析 + 语法高亮 + 错误位置点击跳转 + 5 项统计,数据本地化不上传

8 个常见 Foot-Guns

Foot-Gun 1:trailing comma 被标准 JSON 拒绝

症状:从 Chrome DevTools 「Copy as fetch」复制的 response body,粘贴到 JSON formatter 报「Unexpected token」。你以为是工具的 bug — 实际上是因为 DevTools 复制的 JSON 带了 trailing comma(最后一个 key 后多一个逗号),而标准 JSON 不允许 trailing comma。

为什么:ECMAScript 2017+ 允许对象字面量的 trailing comma,所以 Chrome / Firefox 的 DevTools 复制出来的 JSON 经常带逗号。Python 3.7+ 字典也允许。但 JSON 规范 RFC 8259 第 5 章明确写「trailing commas are not allowed」。naive 的 JSON.parse 严格遵循规范,看到 {"a": 1,} 就报错。

修复:生产环境的 JSON 必须无 trailing comma。开发环境可以用工具的「容错解析」选项 (Piick JSON 格式化工具支持 JSON5 风格,接受 trailing comma + 单引号 + 注释),但提交到 API / 配置文件前必须 lint 掉。CI 跑一遍 prettier --check 或 eslint --rule no-trailing-commas 兜底。

真实场景:前端 fetch 后端 API,Chrome DevTools 「Copy as fetch」,粘贴到 jq 跑 jq .response.users[0].id,报错。原因是 DevTools 输出的 JSON 带 trailing comma,qj 是 strict 解析。永远用 jq 之前先 lint 一次,或者直接用 jq 容错模式 jq -R .。

Foot-Gun 2:Unicode 字符没转义

症状:后端返回 {"name":"张三"},前端 JSON.parse 没问题。但有时候同样的数据粘贴到工具报「Unexpected token」,或者某些库 stringify 出来的 JSON 字符串里中文变 ?。这是 Unicode 转义不彻底或不一致。

为什么:JSON 标准规定字符串中的非 ASCII 字符可以直接以 UTF-8 出现(因为 JSON 文档本身要求 UTF-8),也可以转义成 \uXXXX。两种格式都合法。但有些库的默认行为不一致:

  • Python json.dumps 默认 ensure_ascii=True,把所有非 ASCII 转成 \uXXXX
  • JavaScript JSON.stringify 默认保留原始 Unicode
  • Java ObjectMapper 默认转义
  • Go encoding/json 默认转义

后端 Python 输出 {"name":"\u5f20\u4e09"},前端 JS 解析得 张三,但你复制到不支持 \uXXXX 解析的工具里就看到一堆转义码。

修复:永远确保生产环境 JSON 是 UTF-8。Python 后端用 json.dumps(..., ensure_ascii=False) 输出可读格式(配合 Content-Type: application/json; charset=utf-8)。调试时用工具的容错解析,把 \uXXXX 当作合法输入。

真实场景:Python 后端 dump 出来 {"msg":"\u4f60\u597d"},前端 fetch 后 response.json() 拿到 {msg:"你好"},正确。但你把后端原始响应粘贴到 vim 里复制给同事,同事看到 {"msg":"\u4f60\u597d"} 一脸懵。这就是 UTF-8 vs \u 转义的不一致。

Foot-Gun 3:大整数精度丢失(Snowflake ID 末位变 0)

症状:后端返回 {"user_id": 1234567890123456789},前端 JSON.parse 后 user_id 是 1234567890123456800,末位 3 位都是 0。后端给的 ID 是对的,前端取出来的 ID 是错的,数据库查询 404。

为什么:JavaScript Number 是 IEEE 754 双精度浮点数,安全整数范围是 2^53 − 1 = 9007199254740991(约 9 × 10^15)。超过这个范围的整数会丢精度。Snowflake ID 是 19 位整数(约 10^18),JSON.parse 直接丢精度。Python int / Java long 没有这个问题,但 JS 是最常用的前端语言。

修复:

  • 后端:bigint 序列化成字符串("1234567890123456789"),而不是数字
  • 前端:解析时用 json-bigint 库,或者全部按字符串处理
  • 展示:UI 显示时再 BigInt(id).toString(),计算时用 bigint 类型
  • API 设计:返回 ID 时考虑直接用 UUID(36 字符)或 ULID(26 字符),都是字符串格式

用 Piick JSON 格式化工具校验 API 响应时,先看 tools.jsonFormatter.input 的 placeholder,粘贴后看输出是否一致;如果 ID 长度 > 15 位,警惕大整数精度问题。

真实场景:Twitter Snowflake ID 是 64-bit 整数,约 1770000000000000000,远超 2^53。前端 fetch tweet id 1234567890123456789,实际拿到 1234567890123458000,查不到。Snowflake ID 必须用字符串序列化。

Foot-Gun 4:缩进 ≠ 嵌套关系

症状:你的代码用 2 空格缩进,同事的代码用 4 空格缩进,PR review 看着完全不一样的 JSON,但解析结果完全一样。你以为是嵌套深度不同,实际是 {"a":{"b":{"c":1}}} 和 {"a": { "b": { "c": 1 } }} 是同一个对象。

为什么:JSON 的语法不在乎空格,只要符合 RFC 8259 词法规则,任何数量的空格 / tab / 换行都是等价的。缩进只是「人眼可读性」,跟实际嵌套无关。这也是为什么 JSON 文件用 git diff 时,即使加了 / 删了一个字段,看起来可能整片都都变了(因为缩进重排)。

修复:统一缩进风格(推荐 2 空格,因为更省行宽)+ 用 prettier / jq 自动格式化,避免人手格式化。用工具的「缩进」选项 (Piick JSON 格式化工具支持 2 空格 / 4 空格 / tab / minify),团队统一一种。

真实场景:JSON 配置 100 个字段,2 空格和 4 空格对比:

  • 2 空格:{"a":1,"b":2,"c":{"d":3}} 紧凑
  • 4 空格:{"a": 1, "b": 2, "c": { "d": 3 }} 宽松

嵌套深度 5+ 时,4 空格每层 4 字符,5 层嵌套 = 20 字符左边距,屏幕宽 80 字符只剩 60 字符内容。2 空格更适合深嵌套。

Foot-Gun 5:BOM 字符被忽略

症状:JSON 文件明明在 VSCode 里看着正常,粘贴到工具 / Node.js JSON.parse 报错「Unexpected token」。右键 → 查看编码 → UTF-8 with BOM。原因是文件开头有一个隐形字符 \ufeff(BOM, byte order mark)。

为什么:Windows 记事本 / 部分老编辑器保存 UTF-8 文件时,默认加 BOM(3 字节 EF BB BF)。JS / Python / 大多数 JSON 解析器看到 \ufeff 当 JSON token 就报错。JSON 规范不允许 BOM 在 JSON 文本里(除了在 JSON 文档字符串内部,作为转义字符)。

修复:

  • 保存时无 BOM:VSCode 右下角 → UTF-8 with BOM → UTF-8(默认)
  • 命令行转码:sed '1s/^\xef\xbb\xbf//' input.json > output.json
  • Python 读取:用 encoding='utf-8-sig' 自动剥离
  • Node.js:用 fs.readFileSync(path, 'utf-8').replace(/^\ufeff/, '')

用 Piick JSON 格式化工具时,粘贴含 BOM 的 JSON 会报「Unexpected token」,手动删 BOM 或用 sed 预处理。

真实场景:Windows 同事写 JSON 配置 → 保存 → 提交 → CI 跑 JSON.parse → 报错。CI 不会自动 strip BOM,需要在源头改编辑器设置。.editorconfig 里加 insert_final_newline = true 和「编码 UTF-8 无 BOM」,团队统一。

Foot-Gun 6:单引号当 JSON

症状:Python 字典 {'a': 1, 'b': 2} 直接复制到 JSON formatter 报错。Python 字典默认输出格式跟 JSON 很像,但用单引号,不是合法 JSON。

为什么:JSON 标准明确规定字符串必须用双引号,单引号不行。Python 字典 repr() 输出单引号,JSON 字符串输出双引号:

新人很容易把 print(d) 当 JSON 用,粘贴到工具就报错。

修复:

  • Python 调试时用 print(json.dumps(d, indent=2)) 而不是 print(d)
  • 工具的「容错解析」选项接受单引号(Piick JSON 格式化工具支持 JSON5 风格)
  • 教育团队「Python dict ≠ JSON,Python str 用单引号,JSON 用双引号」

真实场景:Python 新人 debug API 响应,print(response) 输出 {'code': 200, 'msg': 'ok'},以为是 JSON 复制给后端同事,后端同事 JSON.parse 报错。新人 debug 第一坑。

Foot-Gun 7:NaN / Infinity / undefined 序列化

症状:JS 对象 {a: NaN, b: Infinity, c: undefined} 用 JSON.stringify 序列化后是 {"a":null,"b":null}(NaN 和 Infinity 变 null),undefined 字段直接消失。如果后端用 strict JSON parser(比如 Python json.dumps 再 json.loads),行为不一致:Python json.dumps 序列化 float('nan') 是 NaN(非标准),json.loads('NaN') 报错。

为什么:JSON 标准不允许 NaN / Infinity / undefined,但 JS 是日常用这些的语言:

  • NaN / Infinity:JS 全局常量,数值计算错误时产生
  • undefined:JS 「变量未定义」的标志
  • JSON.stringify:把它们都当 null 处理(undefined 直接丢字段)
  • JSON.parse('NaN'):JS 是宽容的,接受为 NaN(非标准)
  • json.dumps(float('nan')):Python 输出 NaN(非标准)
  • json.loads('NaN'):Python 默认 strict,报错

跨语言数据交换时,NaN / Infinity / undefined 的序列化行为不一致,容易出 bug。

修复:

  • API 规范:永远不使用 NaN / Infinity,无效值用 null 或省略
  • 数据清洗:序列化前手动 if (x === undefined || Number.isNaN(x)) x = null“
  • 后端严格校验:Python json.dumps(..., allow_nan=False),报错时直接 fail-fast
  • 库:JSON Schema validation 在 API 入口校验,拒绝 NaN / Infinity

用 Piick JSON 格式化工具时,粘贴含 NaN / Infinity 的 JSON 会报「Unexpected token n」(因为 NaN 不是合法 token),手动替换为 null。

真实场景:前端 fetch 后端 API,响应 {score: NaN}(后端 bug),前端 JSON.parse 后 score: NaN,SQL 查询 WHERE score > 0 不返回任何行(NaN > 0 是 false),UI 显示「无数据」,调试半天才发现是 NaN。永远不要传 NaN,后端做防御性校验。

Foot-Gun 8:排序键改变了 Git diff / 语言语义

症状:你把工具的「sort keys」打开,JSON 看着整齐了,git commit 后同事 review 看到 100 行 diff,全是字段顺序变化,没有实际内容变化。或者更严重的:Go map / Python 3.7+ dict 在某些边缘情况下对 key 顺序敏感,排序后语义变了。

为什么:JSON 对象是 unordered map 的标准定义,但 RFC 8259 实际允许实现保留顺序(因为 JS 对象字面量、Python 3.7+ dict 都是有序的)。工具的 sort keys 选项是为了显示好看,但:

    1. Git diff 噪音,影响 review 质量
    1. 对 key 顺序敏感的序列化协议(JSON Canonicalization Scheme)出错
    1. 某些库的 Object.keys(obj) 顺序依赖插入顺序,排序后 forEach 顺序变了

修复:生产环境的 JSON 不要自动排序键。开发期用工具看结构就好,提交前用 jq 或 prettier 重新重新format(保留原始顺序)。如果团队真的要统一顺序,用 lint 强制(sort-package-json for package.json),不要依赖工具运行时排序。

用 Piick JSON 格式化工具时,默认关闭 sort keys,只在「我要看结构」时手动开启。

真实场景:同事打开 sort keys,提交 1000 行 package.json 全部重排,reviewer 看 diff 看到 800 行是顺序变化,200 行是新增依赖,2 小时 review 全部浪费。不要在生产配置上用 sort keys。

工具选型决策

选项 A:IDE 内置格式化(VSCode / WebStorm)

适合:每天写 JSON、已经用 IDE 的人。

优点:实时格式化、不离开编辑器、跟 schema 集成(自动补全 key)。缺点:只能本地 JSON、不能跨工具同步(IDE 配的格式跟 jq 配的不一样)、不支持批量处理(团队 CI 自动格式化得另搭一套)。

选项 B:命令行工具(jq / python -m json.tool)

适合:CI / build pipeline 里自动格式化、有 shell 环境的项目。

优点:脚本化、可批量、可结合 lint、CI 集成。缺点:不直观(文本输出没颜色)、不支持 syntax highlight、不支持容错解析(JSON5 / 单引号 / 注释)。

选项 C:Piick JSON 格式化工具

适合:跨工具调试(API 响应 + 配置文件 + JWT payload)、不想装额外 npm 包(纯前端项目)、敏感数据(生产 API 配置 / 含 PII 的 JSON)不想过任何第三方服务器。

优点:零依赖(纯前端 JSON.parse + tokenizer)、语法高亮 + 排序键 + 容错解析(JSON5 / 单引号 / 注释)+ 错误位置点击跳转 + 5 项统计(键 / 数组 / 最大嵌套深度 / 字节)、数据本地化。缺点:不能集成到 CI(用选项 B 补)、不能自动 save(纯前端工具)。

决策树:

  • 已经在写 JSON → 选项 A(IDE 插件)
  • CI 集成 + shell 环境 → 选项 B(jq)
  • 跨工具调试 + 敏感数据 + 不装东西 → 选项 C(Piick)

5 个真实场景

场景 1:API 响应调试

前端调后端 API,response 是压缩的 minified JSON,粘贴到工具自动美化 + 折叠 / 展开 + 显示统计(键 / 数组 / 深度)。

步骤:

  1. fetch API → response.text() → 粘贴到 Piick JSON 格式化工具
  2. 自动 syntax highlight(5 种 token 类型)
  3. 看顶部 stats:{keys: 47, arrays: 12, maxDepth: 4, bytes: 1024}
  4. 错误位置点击跳转:JSON.parse 报错时点错误 banner,跳到失败的字符位置
  5. 复制格式化结果 / 折叠嵌套层级

真实建议:永远用工具看 API 响应,不要直接在 console.log 看 minified JSON,人眼读不动。

场景 2:package.json / 配置文件校验

写 package.json 时 typo(多打逗号 / 缺右括号 / 字符串没闭合),CI npm install 报错,但编辑器不报错(IDE 容错)。用工具跑一遍校验。

步骤:

  1. 复制 package.json 内容 → 粘贴到工具
  2. 工具报错 → 看「Line X, column Y」错误位置 → 点击跳转 → 修正
  3. 特别检查::"scripts:","dependencies","devDependencies" 三处(最容易打错)
  4. 提交前跑 npm install --dry-run 二次确认

真实建议:用 Piick JSON 格式化工具的「容错解析」选项(tolerant)在开发期期看「语法错误但接近正确」的提示,比 strict parser 友好。

场景 3:JWT payload 解码

JWT 是 base64url 编码的 JSON,header.payload.signature 三段。解码 payload 看 claims 时,先用 Piick JWT Decoder(同 doc cluster)拆 base64,再用 JSON 格式化器校验解码后的 JSON 是否合法。

步骤:

  1. JWT eyJhbG...sw5c
  2. Piick JWT Decoder拆三段
  3. payload 是 {"sub":"1234567890","name":"John Doe","iat":1516239022} ← minified JSON
  4. 粘贴到 Piick JSON 格式化工具美化 + 看时间戳字段(iat / exp / nbf 转可读时间)
  5. 检查 exp 是否过期

真实建议:JWT 调试链:JWT Decoder → JSON Formatter → Unix Timestamp Converter(同 doc cluster),3 件套覆盖完整流程。

场景 4:Snowflake ID / 大整数 ID 序列化

后端用 int64 返回 ID,前端 fetch 后 id.toString() 末位变 0。字符串化处理。

步骤:

  1. 后端 Python json.dumps({"id": int(2**60)}) → 输出 {"id": 1152921504606846976} ← 末位可能丢精度
  2. 修复:后端 json.dumps({"id": str(2**60)}) → 输出 {"id": "1152921504606846976"} ← 字符串,前端按字符串处理
  3. 前端 BigInt(id).toString() 做数学运算
  4. UI 显示 id.slice(0, 4) + "..." + id.slice(-4)(类似银行卡号显示)

真实建议:API 规范:大整数 ID 永远用字符串。前端永远不要 assume ID 是 Number。

场景 5:i18n 资源文件 / 多语言字典

i18n JSON 文件含中文 / 日文 / emoji,正确处理 Unicode 转义。开发期用工具可视化,生产期用 UTF-8 + ensure_ascii=False 输出可读格式。

步骤:

  1. i18n 文件 zh.json:{"hello":"你好,世界!🌏"} ← 直接 UTF-8
  2. 粘贴到 Piick JSON 格式化工具验证 UTF-8 字符正常显示
  3. 后端输出用 json.dumps(..., ensure_ascii=False)(Python)或 JSON.stringify(JS 默认)
  4. 响应头:Content-Type: application/json; charset=utf-8
  5. CI 加 lint:eslint-plugin-unicorn 或自定义脚本检查 ensure_ascii 配置

真实建议:永远不要在 JSON 字符串里写 \uXXXX 转义,开发期用 UTF-8 直接写可读字符。生产期让框架 / 库决定怎么序列化,自己配置 ensure_ascii=False。

推荐做法

  • 永远用工具校验 JSON(尤其是生产 API 配置),不依赖 IDE 容错
  • 永远容错解析开发期(工具的 tolerant 模式),production lint 无 trailing comma + 无注释 + 无单引号
  • 大整数 ID 永远用字符串序列化(Snowflake ID / 分布式主键),不假设前端 Number 安全
  • 不要自动排序键(影响 Git diff 和 key 顺序敏感语言),只在「看结构」时手动开启
  • 统一缩进(推荐 2 空格)+ 用 prettier / jq 自动格式化,避免人手格式化
  • 永远用 UTF-8 + Content-Type charset=utf-8,不要用 \uXXXX 转义(开发期可读性差)
  • CI 跑 JSON Schema validation,在 API 入口拒绝 NaN / Infinity / 严格模式字段缺失
  • Windows 编辑器关闭 BOM,.editorconfig 团队统一,避免 BOM 字符在 CI 报错
  • 用 Piick JSON 格式化工具在线格式化 / 校验 / 美化,语法高亮 + 容错解析 + 错误位置点击跳转 + 5 项统计 + 数据本地化不上传。搭配 Piick JWT 解码器拆 base64、Piick SQL 格式化工具格式化 SQL、Piick URL 编码解码器处理 URL 参数、Piick Cron 生成器生成 cron 表达式、Piick 正则测试器测试 JSON 字段值正则 — 5 件套覆盖 JSON 上下游所有 dev 场景

JSON 格式化不是美化,是 API / 配置 / 日志的可读性基础设施。真正的「格式化对」 = 标准 JSON + UTF-8 + 大整数字符串 + 不排序 + 不带注释 + 不带 trailing comma + 团队统一缩进。把 Piick JSON 格式化工具 收藏在书签栏,调试 API / 校验配置 / 解析 JWT payload 时开一下 1 分钟走完,日常 dev 提效 30%。