使用方法
- 把 JSON 粘进上面的输入框,或直接在里面敲。页面打开时已经放了一段 8 行、11 个值、最大嵌套 2 层的合法示例,右侧统计卡显示的就是它的结果。
- 在输入框里按 Ctrl + Enter(macOS 用 Cmd + Enter)立刻校验一次;单独按 Enter 是换行,因为我们不希望你在手工排版 JSON 时被反复打断。
- 校验不通过时,「校验结论」里会给出第几行第几列,下面贴出出错那一行的原文和一个
^指示符,指到具体是哪一列;再往下是浏览器原生JSON.parse对同一段文本的说法,便于对照。 - 点「载入错误示例」会填入一段故意写错的 JSON——第 3 行的数组里多了一个逗号,本工具报的是 3:25,而浏览器原生报错只给出一小段截断的文本,没有任何数字可循。
- 下方的 19 条对照表是同一个状态机跑出来的历史样例,点「加载更多」展开全部,「导出 CSV」拿到带 BOM 的表格文件,「复制对照表」直接以 TSV 形式进剪贴板。
这个 JSON校验工具能解决什么问题
一是把「解析失败」翻译成能动手改的位置。接口返回 400 时经常只有一句 Unexpected token ']', "[1,2,]" is not valid JSON,连行号都没有;浏览器控制台里的提示同样时有时无。这个 JSON校验工具用逐字符状态机把出错点换算成第几行第几列,再配一句中文的原因说明,你直接跳到那一列改就行。
二是把「语法错」和「值不对」分开。校验只管语法:字段该不该是数字、时间字符串格式对不对、枚举值合不合规,这些语法层面看不出来,本页也不判。反过来说,一段语法完全合法却被服务端拒绝的 JSON,问题不在校验这一步,别在这里空转。
三是核对程序拼出来的 JSON。配置文件、埋点上报、导出报表常常是代码拼串生成的,肉眼很难看出少没少一个逗号。先过校验,再看值的个数、最大嵌套深度和行数对不对——示例里 8 行文本对应 11 个值、2 层嵌套,数字对不上就说明结构和你以为的不一样。
四是拿真实样例反推写法。对照表里 19 条都是日常最容易踩的坑:尾随逗号、单引号键名、前导零、NaN、字符串里的真实换行……每条都标了状态机检出的行列,你可以照着把手上那段 JSON 的症状对号入座。
本页不做这些事:不按缩进重新排版、不比较两份 JSON 的差异、不做压缩、不做 JSONPath 取值、不检查重复键、不做 JSON5 或 JSONC 这类方言的完整解析。校验通过之后想接着排版,页面底部的「JSON格式化工具」更合适;想按路径取值请用「JSONPath查询」。
校验原理:逐字符状态机、行列换算与 JSON.parse 位置映射
核心是一个递归下降的逐字符状态机。它从左到右把文本走一遍:遇到 { 进入对象上下文、遇到 [ 进入数组上下文、遇到 " 进入字符串上下文,每个上下文里只接受 RFC 8259 规定的那几种后继字符。整个扫描是单次遍历,不建抽象语法树,所以 100 万字符的输入也不会明显卡顿,内存只跟嵌套深度有关。字符串上下文里单独处理转义:只有 \" \\ \/ \b \f \n \r \t 这 8 个单字符转义和 \uXXXX 合法,与 RFC 8259 第 7 节给出的 ABNF 一致;\u 后面必须恰好 4 位十六进制数字,\u4E 会被判非法。
行列是怎么算出来的。扫描时一直维护 i、line、col 三个游标,初始值都是 1,也就是行号列号都从 1 开始而不是从 0 开始。每读一个字符:换行把 line 加一并把 col 归 1,其余字符把 col 加一。\r\n 只让行号加一次,单独的 \r 或单独的 \n 也各算一次换行,所以 Windows 和 Unix 行尾混在一份文件里也不会算乱。出错时抛出的 line、col 是那个不合法字符本身的位置,不是它后面的位置。
为什么不能只靠 JSON.parse 的报错。V8 的报错有时带位置、有时完全不带:{"a":1,} 会给出 Expected double-quoted property name in JSON at position 7 (line 1 column 8);但 [1,2,] 只给一句 Unexpected token ']', "[1,2,]" is not valid JSON,一个数字都没有。所以本页把状态机当主定位器,把 JSON.parse 当复核,两边的说法并排显示在结论里。
position 到行列的换算是另一条独立路径。V8 的 position 是从 0 开始的 UTF-16 码元偏移,不是码点、更不是字节。换算方法是:行号 = position 之前出现过的换行符个数加 1;列号 = position 减去它前面最近那个换行符的下标。因为 JavaScript 字符串下标本身就是码元,一个 😀(占 2 个码元、1 个码点)会让它后面所有列号比你在编辑器里数出来的多 1——这不是算错,而是「列」在 JS 世界里按码元定义。
UTF-8 字节数按码位区间累加:≤ U+007F 记 1 字节,U+0080–U+07FF 记 2 字节,U+0800–U+FFFF 记 3 字节,代理对(补充平面,如 emoji)记 4 字节。示例文本里 6 个汉字各多算 2 字节,所以 136 个码点对应 148 个 UTF-8 字节;这段文本没有补充平面字符,所以按 UTF-16 码元数也是 136。
完整示例(数值仅作演示)
示例条件与页面打开时的默认值完全一致:一段合法 JSON,未做任何修改。状态机走完后的六项实测值是:
- 值的个数 11:只数值,键名不算。拆开看是 3 个字符串(
"柳泉工具"、"json"、"校验")、3 个数字(2、1001、98.5)、1 个布尔true、1 个null,再加上根对象、tags数组、author对象这 3 个容器,3 + 3 + 1 + 1 + 3 = 11; - 最大深度 2:顶层对象算第 1 层,
tags数组和author对象嵌在它里面,算第 2 层; - 顶层类型 对象:第一个非空白字符是
{; - 行数 8、码点数 136、UTF-8 字节 148:中文在 UTF-8 里每字 3 字节,所以字节数比码点数大。
同一段文本丢给浏览器原生的 JSON.parse,结论也是通过——两边一致才说明状态机没有误判。
把示例换成一段故意写错的文本,差别就出来了。下面这段第 3 行的数组里多了一个逗号:
{ "name": "柳泉工具", "tags": ["json", "校验",], "version": 2 }
状态机报出的是 3:25——第 3 行第 25 列,也就是那个 ]。JSON.parse 对同一段文本只给出一句 Unexpected token ']', ... is not valid JSON,没有任何数字可循。这就是本页存在的理由:它把「错了」变成「错在第 3 行第 25 列」。
合法与非法写法对照表
下表按 RFC 8259 的 ABNF 逐条列出常见写法。若你在别处看到「JSON 可以写注释」,那说的是 JSONC 或 JSON5 这类扩展,不是标准 JSON。
| 写法 | 结论 | 依据 |
|---|---|---|
{"a":1} | 合法 | 标准对象,成员名必须是双引号字符串 |
{"a":1,} | 非法 | 逗号只能出现在两个成员之间,} 前不能有逗号 |
{a:1} | 非法 | 成员名必须是 string,裸标识符不是 |
{'a':1} | 非法 | string 只能用双引号,单引号是 JS 写法 |
{"a":01} | 非法 | number 的整数部分不允许前导零,0 必须单独出现 |
{"a":0.5} | 合法 | 小数部分 .5 合法,但整数部分的 0 不能省 |
{"a":.5} | 非法 | number 必须有整数部分 |
{"a":1.} | 非法 | 小数点后至少要有 1 位数字 |
{"a":1e3} | 合法 | 指数部分为 e 或 E,可带正负号 |
{"a":1e} | 非法 | 指数部分必须有数字 |
{"a":+1} | 非法 | number 只允许前导减号,正数不加号 |
{"a":NaN} / Infinity | 非法 | 不是 number 产生式,需写成 null 或字符串 |
{"a":undefined} | 非法 | 标准 JSON 没有 undefined |
| 字符串里放真实换行 | 非法 | string 内不得出现未转义的控制字符,要写成 \n |
{"a":"\u4E2D"} | 合法 | \u 加恰好 4 位十六进制 |
{"a":"\u4E"} | 非法 | 不足 4 位,解析器会继续吃后面的字符直到凑满或报错 |
{"a":1} 后接 // 注释 | 非法 | RFC 8259 没有注释产生式 |
{"a":1}{"b":2} | 非法 | 一个 JSON 文本 = 一个值,值后面只允许空白 |
输入范围、边界与精度说明
- 行号列号都从 1 开始,列号按 UTF-16 码元计。这是最容易和编辑器对不上的地方:如果出错位置前面有一个 emoji 或扩展 B 生僻字,列号会比编辑器显示的大。结论里同时贴出错行原文与
^指示符,以那个为准最稳妥。 - 定位的是「第一个不合法的字符」。有些错误要到后面才确认得下来,最典型的是尾随逗号:读到
,时还不能断定它多余,必须再往后跳过空白、看到}或]才能确定,所以位置报在右括号上。{"a":1,}报 1:8、[1,2,]报 1:6,和 Chrome 给出的 position 是同一种口径。 - 深度的定位方式不同。对象与数组每进入一层就加一,顶层算第 1 层,示例的最大深度是 2。嵌套过深时状态机本身没问题,但各语言解析器几乎都有栈上限:V8 在万级层数附近会抛
RangeError: Maximum call stack size exceeded,那是引擎栈限制而不是语法问题,语法上它仍然合法。 - 数字精度:语法合法 ≠ 值保真。RFC 8259 对数字的位数没有任何限制,而
JSON.parse用 IEEE 754 双精度浮点存储,安全整数上限是 253 − 1 = 9007199254740991。写{"id":9007199254740993}语法完全正确,本页判通过并会额外提示一句,但解析出来的值会变成 9007199254740992。要一字不差就把它写成字符串"9007199254740993"。 - 文件开头的 BOM 会被自动忽略。Windows 记事本「另存为 UTF-8」会在最前面写一个看不见的
U+FEFF,JSON.parse读到它会直接报Unexpected token '\uFEFF'。本页把首字符是U+FEFF时替换成一个空格再校验,既不改变行列,也不会让它冒充语法错误。 - 重复键不报错。
{"a":1,"a":2}语法合法,JSON.parse保留最后一个。这属于值语义,需要按你的业务规则另外检查。 - 孤立代理项不报错。
"\uD83D"是合法的 4 位十六进制转义,JSON.parse也照收,但它不是一个有效的 Unicode 标量值。语法层面它没错。 - 输入上限 100 万字符(输入框
maxlength,按 UTF-16 码元计)。超过之后输入框不再接收新内容,已经在里面的文本不会被截断。校验是单次遍历,100 万字符通常在一两百毫秒内出结果。
常见问题
为什么浏览器控制台的报错有时带 position、有时只有一段文本?
因为 V8 对不同语法错误给的线索不一样。{"a":1,} 这种「属性名位置读到 }」的错误会附带 at position 7 (line 1 column 8);而 [1,2,] 只会吐出 Unexpected token ']', "[1,2,]" is not valid JSON,还把片段截断成十来个字符。本页的状态机不依赖引擎文案,任何一类错误都给出确定的行列。
尾随逗号为什么报在 } 或 ] 上,而不是那个逗号?
因为读到逗号的那一刻无法判断它是不是尾随——{"a":1, "b":2} 里同样的逗号完全合法。只有继续跳过空白、看到右括号,才能确定「这个逗号后面没有成员了」。所以位置落在右括号上。{"a":1,} 报 1:8,逗号在第 7 列;这跟 Chrome 控制台给出的 position 7 是同一种口径。
校验通过,是不是就说明这段 JSON 没问题了?
只说明语法没问题。重复键、超出 253−1 的整数、孤立代理项、字段该不该为 null,这些本页都不判。反过来也要留意:一段语法合法的 JSON 被服务端拒绝,问题往往在字段类型或业务校验上,不在语法这一步。
{"a":1,"a":2} 这种重复键为什么不报错?
因为 RFC 8259 只规定「成员名应当是唯一的」,把重复时的取舍留给实现,并不要求解析器报错。JavaScript 的 JSON.parse 保留最后一个,Java 的 Jackson 默认也是后者覆盖前者。要查重复键得按成员顺序再扫一遍,那是另一类工具的事。
为什么这个 JSON校验工具不像别的工具那样自动帮我排版?
因为两件事的判定标准不同。排版要重建缩进和换行,属于输出格式问题;校验只回答「这段文本能不能被解析器接受」,必须在原始字符序列上按行列走,一旦重排,报出来的行列就不再对应你手上的文件了。所以本页刻意不改动你输入的任何字符,连 BOM 也只替换成一个空格。
文件开头有个看不见的字符,是它让校验失败的吗?
很可能。Windows 记事本「另存为 UTF-8」会写入一个 U+FEFF 字节顺序标记,肉眼看不到,但 JSON.parse 会把它当成非法字符。本页遇到首字符是 U+FEFF 时会先把它换成空格再校验,并在补充说明里点出来。要彻底清掉,可以用编辑器另存为「UTF-8 无 BOM」。
超出 253−1 的大整数为什么判为合法、解析出来却变了?
因为校验只管语法,而 JSON.parse 会把数字转成 IEEE 754 双精度浮点,只能精确表示到 9007199254740991。9007199254740993 语法没错,本页判通过,但 JSON.parse 之后会变成 9007199254740992。要原样保留就得写成字符串,或改用支持任意精度整数的解析库(例如 Python 的 json 会保留大整数,Java 需要开 USE_BIG_INTEGER_FOR_INTS)。
一次能校验多大的文件?超限会怎样?
输入框上限 100 万字符,按 UTF-16 码元计。超过之后输入框不再接收新内容,已输入的部分不会被截断;扫描是单次遍历,100 万字符一般在一两百毫秒内出结果。如果文件更大,建议先用命令行工具(如 python -m json.tool 或 jq)过一遍,再用本页定位具体那一段。
相关工具
- CSV转JSON转换器:先把表格数据转成 JSON,再拿回来校验语法对不对。
- JSON行转换器:一行一个 JSON 的 NDJSON / JSON Lines 格式,本页无法一次校验多份文档,那种场景用它。
- JSON转XML转换器:语法确认无误后再换格式。
- JSONPath查询:按路径把深层字段取出来,省去一层层展开。
- JSON差异工具:两份结构大体相同的 JSON 到底差在哪几个字段,用差异工具比人眼快。
- YAML校验:同一套「先过语法」的思路,但对象是 YAML,缩进敏感度完全不同。
- 开发者工具:查看全部开发者工具。