2026年8月9日
JSON 格式化与校验实践:把报错定位到行列,而不是靠猜
每个开发者都有过这样的经历:把一段 JSON 粘进解析器,得到一句 Unexpected token 什么什么,然后对着一整屏的文字发呆,试图肉眼找出问题。报错信息很少用大白话告诉你”错在哪”——但几乎总会告诉你”在哪个位置”,这就够了。这篇指南要讲的就是如何把这个”位置”变成两分钟的修复:格式化为什么重要、哪些错误你真的会遇到、以及如何按行列去读一个报错位置,而不是靠猜。
文中所有操作都可以直接在本站完成。JSON 格式化校验器在浏览器里完成解析,报错带一一起始的行号和列号;文档通过校验后,再用 JSONPath 查询 工具把需要的值抽出来。你粘贴的任何内容都不会离开本机——这一点在调试生产环境的请求体时尤其重要。
格式化 JSON 到底值不值#
机器不在乎 JSON 长什么样。{"a":1} 和同一个对象展开成 40 行的漂亮打印,解析之后逐字节等价——语义相同、合法性相同。格式化是给人看的,而它的回报体现在三件具体的事上。
看差异(diff)。 版本控制按行比较文件。一个压缩成单行的 5000 字符 JSON 文件,任何改动都会产生一行无法阅读的巨大 diff;同样的文档换成两空格缩进,改动就是一小块可以直接评审的内容。配置文件或 API 固定数据如果要进仓库,统一格式不是装饰——这是代码评审能进行的前提。
看清结构。 缩进让嵌套关系可见。当一个对象被放进了错误的数组,或者一个右花括号提前结束了错误的作用域,缩进会立刻显形。而在一整行扁平文本里,同样的错误要等到下游以”缺键”报错时才会暴露。
错误定位。 这是本指南的核心收益。“第 12 行第 9 列有错”这种信息,只有当你的输入真的有行有列时才可用。如果 JSON 是一整行,所有错误都报在第 1 行,列号动辄上千。先格式化、再排查——错误位置立刻变得有意义。
当然也有反面场景:传输体积。用本文实例数据里的一张订单对象算过——含一个客户和两个明细项,两空格缩进格式化后 286 字节,压缩后 179 字节,缩小了 37%。所以通行约定是”仓库和日志里用漂亮格式、线上传输用压缩格式”。两种形态可以无损互转,因为它们都是对同一个解析后的值重新序列化的结果。
格式化器实际做了什么#
理解这一点能省掉日后一整类困惑。真正的格式化器并不”编辑”你的文本,它走三步:
- 用真正的 JSON 解析器解析输入。解析抛异常就到此为止——返回的是校验错误。
- 可选地对解析结果做变换——比如按字母序重排对象键。
- 按你选的缩进把值重新序列化成文本。
由此有两个推论。第一,格式化是”规范化”:它产出的是你数据的规范序列化,不是在你原文里插空格。你以为没问题的注释、从没注意到的重复键、1.50 与 1.5 的写法差异——全部在解析这一步尘埃落定,输出展示的是解析器实际看到的东西。第二,格式化就是”附带输出的校验”:能干净格式化的文档必然是能干净解析的文档,反之亦然。不存在”合法但格式化不了”的状态。
缩进选择是风格问题,但有一条实用边界:两空格和四空格都是空格(在任何终端、任何复制粘贴中不变形),制表符缩进的漂亮输出最紧凑、粘贴进用 tab 的编辑器也正确。每个项目定一个,然后别再讨论。键排序值得开的场景是:比较两份内容可能相同、键序不同的文档——排序序列化让比较回归到纯文本层面。
你真正会遇到的五类错误#
JSON 的语法很小,也毫不留情。实际工作中,你见到的非法 JSON 几乎逃不出这五类。下面每个例子都是真实数据:报错信息来自现代引擎,位置按本站格式化器完全相同的方式计算得出。
一、单引号写成了双引号的活儿。 JSON 的字符串和键必须用双引号;单引号是 JavaScript 的习惯,语法直接拒绝。
{
'host': 'api.example.com',
"port": 443
}
引擎报错:Expected property name or '}'——定位在第 2 行第 3 列,正是第一个单引号。列号指向解析器卡住的那个字符,所以修复通常就是盯着那个点看。
二、尾逗号。 在 JavaScript 的数组与对象里很方便;在 JSON 里非法。
{
"users": [
{"id": 1, "name": "Ada"},
{"id": 2, "name": "Linus"},
],
"total": 2
}
报错 Unexpected token ']'——解析器在上一行的逗号之后期待另一个值,却见到了右方括号。报错位置在 ],但病灶是它前面第 4 行的那个逗号。学会”往前看一个记号”,是读懂这类报错的一半功力。
三、成员之间漏了逗号。 上一类的镜像:
{
"retries": 3,
"timeout": 30
"region": "eu-west"
}
解析器读到 "timeout": 30,期待 , 或 },却遇到了一个字符串:Expected ',' or '}' after property value。位置指向 "region" 的开头——也就是下一个成员开始的行,恰好比你漏写逗号的那行低一行。
四、字符串里出现未转义字符。 字符串值里有真实的换行,或内嵌引号:
{
"note": "line1
line2"
}
引擎报 Bad control character in string literal,位置在 line1 之后。字符串必须把控制字符转义为 \n、内部双引号转义为 \"。有人从日志或聊天窗口里复制 JSON 片段、复制过程断行了,也会出现这个错。
五、数字或字面量写法不合法。 JSON 不允许注释、不允许 NaN、不允许 Infinity、不允许十六进制、不允许前导零。{"ts": 0x1F}、{"n": NaN}、// comment 都是硬性解析错误。报错措辞各异(Unexpected token 'x'、Unexpected token 'N'),但类别相同:语法只认它认的东西。
一个整体观察:这五类全是词法错误——解析器压根没走到结构或内容层面。JSON 在解析期也只有这一种错误,所以错误清单才这么短、这么好记。
行列位置是怎么算出来的#
解析器抛错时会附带一个字符偏移量——从输入开头到失败点的字符数。光秃秃的偏移量对人类极不友好(“position 4”),所以格式化器要做一次换算:从输入开头逐字符走一遍,每读一个字符列号加一,遇到换行符就把列号归零、行号加一。结果就是编辑器行号槽里能直接找到的一一起始行列。
知道这个换算过程,就能解释你可能留意过的两个怪现象。第一,输入只有一行时,任何偏移都映射到第 1 行——把输入格式化(或至少给它断行),位置立刻可用。第二,如果引擎压根没法从报错信息里提取偏移量,一个靠谱的校验器会退化为扫描或报告”输入末尾”,而不是编造一个位置。那种不管实际错误在哪、一律报”第 1 行第 1 列”的校验器不是在定位你的错误,是在猜。
排查顽固文档的实操流程:粘进格式化器,读出行列,在编辑器里跳过去,修掉,重复。大文档往往不止一个错——因为作者会把同一个错误(比如尾逗号)在好几处都犯一遍——每修一个就暴露下一个位置,来回三四轮很正常。
实例演练:先格式化,再查询#
文档一旦能解析,格式化只是开始——多数时候你要的是从中抽取点什么。这正是查询语言的位置。看这张订单对象,两空格缩进格式化(286 字节,压缩 179 字节):
{
"order": "A-3788",
"customer": {
"id": 9042,
"email": "[email protected]"
},
"items": [
{ "sku": "KB-01", "qty": 1, "price": 129.00 },
{ "sku": "CB-02", "qty": 2, "price": 18.50 }
],
"total": 166.00,
"currency": "USD"
}
对这份文档的 JSONPath 表达式与经过验证的结果:
$.total返回166,JSON Pointer 为/total$.customer.email返回[email protected]$.items[*].sku返回两个 SKU——KB-01和CB-02,分别在/items/0/sku与/items/1/sku$..price递归查找任意深度的价格:129和18.5$.items[?(@.qty > 1)].sku按条件过滤,返回CB-02——数量大于一的那个明细项
注意最后两个例子。递归下降 $.. 回答的是”这个键在文档的任何地方出现在哪”——在不是你写的配置文件里查键,这是神器。过滤表达式 [?(@.qty > 1)] 回答”哪些条目满足条件”——这种查询平时要写五行脚本。一个能在格式化输出里高亮匹配跨度的查询工具,等于把 JSON 调试变成了可视任务:路径告诉你匹配了什么,高亮告诉你它在哪。
在本站试一试#
两个工具覆盖上述全部流程,且都在浏览器里运行——无上传、无后端,粘真实报文也安全。
先上 JSON 格式化校验器。把上面五类坏例子粘进去(单引号和尾逗号那两个要手敲一遍),看报错带着行列号回来;然后粘订单文档,在两空格、四空格、制表符之间切换,开关键排序看规范化的排序形态(顶层键变为 currency、customer、items、order、total),再对比压缩与漂亮格式的字节数。
然后带着同一张订单文档去 JSONPath 查询工具。把上面五条表达式各跑一遍核对结果;匹配值会在格式化文档的对应位置高亮。当一份真实的 API 响应摆到你桌上,这套”格式化校验 + 查询抽取”的组合能替代大半为看一眼内容而写的临时脚本。
常见问题#
JSON 缩进用 tab 还是空格?#
都合法,一致性才重要。两空格是现代工具链里最常见的约定,在嵌套很深的文档里不会太宽;tab 的漂亮输出最紧凑。既然任何格式化器都能无损互转,加一道自动化检查(格式化该文件,若有变化则构建失败)就能永久终结这场争论。
尾逗号为什么算错?看起来无害。#
因为语法定义里逗号是成员之间的分隔符,} 前的逗号等于承诺了后面还有一个成员、但它没来。有些解析器提供宽松模式,在解析前剥掉尾逗号——读第三方改不了的文件时有用;但在自己的流水线里默认开宽松,会把真正的错误也藏起来,比如某成员被删掉、逗号忘了删。
JSON 里键的顺序重要吗?#
对合规的解析器不重要——对象按定义是无序的。对人和 diff 重要:稳定、有序的键让两份内容相同的序列化在文本上也相同,比较和评审因此变得简单。键排序选项做的正是这件事:规范化,不改语义。
我的 JSON 合法但特别大,有什么建议?#
排查用漂亮格式、传输用压缩格式、看内容用查询而不是滚动。一份大到读不动的文档,恰恰是”查询表达式($..price)+ 跨度高亮”胜过肉眼逐行的时候。另外注意,超大的单行输入会让错误列号大得离谱——先给它断行再排查。
JSON 里能写注释吗?#
不能——语法里没有注释这个产生式。所以需要注释的配置格式要么扩展 JSON(在自己的解析器里加注释支持),要么干脆换格式。解析前用正则剥掉注释行是常见的临时手段,宽松格式化器做这事更稳一些,但产物已不再是 JSON。
小结#
格式化是”带人类可读输出的校验”:解析这步是正确性的证明,序列化是给你的眼睛、你的 diff、你的评审者看的。报错信息乍看晦涩,但只要认全真正会遇到的五类——引号、尾逗号、漏逗号、未转义字符、不合法字面量——并且记住”报错位置可能指向病灶后一个记号”,它就变得可读。行列定位把引擎的偏移量换算成编辑器能跳转的坐标;查询则把一份通过校验的文档变成答案。
在本站把整个闭环练一遍:先用 JSON 格式化器校验排版,再用 JSONPath 查询工具抽取高亮。