JSON 解析报错看起来经常只有一句话,比如 Unexpected token <、Unexpected end of JSON input、Extra data。这些错误本身并不复杂,但在线上排查时很容易走偏:有人会先怀疑解析库,有人会反复改字段类型,还有人会直接把响应体复制出来找格式问题。
更稳的思路是先判断一件事:你拿到的输入,到底是不是你以为的那段 JSON。
这篇文章按实际排查顺序整理几类常见 JSON 问题,包括接口返回 HTML、空响应、格式不严格、多个 JSON 文档拼在一起、转义错误、结构不匹配、重复 key 和大整数精度问题。
先看输入,不要先看 parse
遇到 JSON 解析失败时,我一般不会第一时间盯着 JSON.parse 或后端反序列化异常,而是先打印这些信息:
js
console.log('status:', response.status)
console.log('content-type:', response.headers.get('content-type'))
const text = await response.text()
console.log('body preview:', text.slice(0, 200))
原因很简单:很多"JSON 解析错误"并不是 JSON 写错了,而是客户端根本没有拿到 JSON。
比如:
- 接口路径写错,返回了 HTML 404 页面
- 网关拦截,返回了登录页
- 服务端异常,返回了错误模板
- 响应为空,但代码仍然执行了解析
- 返回内容被截断,只拿到半段 JSON
先确认状态码、响应头和响应体前几百个字符,可以省掉很多无效排查。
Unexpected token <:你拿到的通常是 HTML
前端最常见的 JSON 报错之一是:
text
Unexpected token < in JSON at position 0
这个错误的关键信息是 <,它通常不是 JSON 里的字符,而是 HTML 的开头。
例如接口真实返回的是:
html
<!doctype html>
<html>
<head><title>404</title></head>
<body>Not Found</body>
</html>
但代码仍然按 JSON 去解析:
js
const res = await fetch('/api/user/detail')
const data = await res.json()
这时 res.json() 读到第一个字符 <,自然会报错。
排查这种问题时,重点看三处:
js
const res = await fetch('/api/user/detail')
const text = await res.text()
console.log(res.status)
console.log(res.headers.get('content-type'))
console.log(text.slice(0, 200))
如果 content-type 是 text/html,或者响应体里出现 <!doctype html>、<html>、<title>,就不要继续查 JSON 格式了,应该先查接口地址、代理配置、登录态、网关规则或服务端错误页。
Unexpected end:响应为空或 JSON 不完整
另一个高频错误是:
text
Unexpected end of JSON input
它的意思是解析器读到结尾了,但 JSON 还没结束。
常见场景有两个。
第一种是空字符串:
js
JSON.parse('')
第二种是 JSON 被截断:
js
JSON.parse('{"name":"Tom","age":')
在线上接口里,空响应经常来自这些情况:
- HTTP 204 本来就没有响应体
- 删除、提交类接口只返回状态码
- 服务端异常导致连接提前断开
- 代理或网关超时,响应体没有完整返回
比较稳的处理方式是先拿文本,再按业务约定判断是否需要解析:
js
const res = await fetch('/api/profile')
const text = await res.text()
if (!text.trim()) {
return null
}
const data = JSON.parse(text)
如果接口约定一定返回 JSON,那空响应就不是前端兜底能解决的问题,应该让服务端统一返回结构,例如:
json
{
"code": 0,
"data": null,
"message": "ok"
}
Expecting property name:把 JS 对象写法当成 JSON
很多人手写 JSON 时,会不小心写成 JavaScript 对象字面量:
js
{
name: 'Tom',
age: 18,
}
这在 JS 代码里可以成立,但不是合法 JSON。
严格 JSON 需要满足几个基本规则:
json
{
"name": "Tom",
"age": 18
}
常见错误包括:
- key 没有用双引号包住
- 字符串用了单引号
- 最后一项多了逗号
- 写了注释
- 使用了
undefined
下面这些都不是合法 JSON:
json
{
"name": "Tom",
}
json
{
"name": 'Tom'
}
json
{
// user name
"name": "Tom"
}
JSON 的定位是数据交换格式,不是 JS 配置文件。如果你需要注释、尾逗号、变量或表达式,那应该考虑 JSON5、YAML,或者直接使用 JS/TS 配置文件,而不是把它们混进 JSON。
Extra data:一次 parse 只能处理一个 JSON 文档
有些后端日志或数据流里,会出现这种内容:
json
{"id":1}
{"id":2}
它看起来像 JSON,但整体并不是一个合法 JSON 文档。因为一次 JSON.parse 只能解析一个完整值,解析完第一个对象后,后面还有额外内容,于是就会出现类似 Extra data 的错误。
正确写法要么是数组:
json
[
{"id": 1},
{"id": 2}
]
要么明确使用 NDJSON,也就是一行一个 JSON:
json
{"id":1}
{"id":2}
如果是 NDJSON,就不要直接用 JSON.parse 解析整个文本,而应该逐行处理:
js
const rows = text
.split('\n')
.filter(Boolean)
.map(line => JSON.parse(line))
这里要注意,普通 JSON 和 NDJSON 是两种不同的数据格式。前后端接口、日志采集、消息队列消费时,要提前约定清楚。
Invalid escape:反斜杠和控制字符问题
JSON 字符串里的反斜杠需要正确转义。
比如 Windows 路径如果这样写:
json
{
"path": "C:\new\test"
}
这里的 \n 会被当成换行,\t 会被当成 tab,结果可能不是你想要的路径。
正确写法是:
json
{
"path": "C:\\new\\test"
}
如果字符串里有换行,也不能直接把真实换行塞进 JSON 字符串:
json
{
"message": "hello
world"
}
应该写成:
json
{
"message": "hello\nworld"
}
这类问题经常出现在日志内容、正则表达式、文件路径、用户复制粘贴的文本里。排查时不要只看格式化后的展示,最好看原始字符串。
JSON 合法,不代表业务结构正确
有时候 JSON 本身是合法的,但业务解析仍然失败。
比如后端期望 age 是数字:
json
{
"name": "Tom",
"age": 18
}
实际传过来的是字符串:
json
{
"name": "Tom",
"age": "18"
}
对 JavaScript 来说,这段 JSON 可以正常解析;但对 Java、Go、Rust 这类强类型服务来说,反序列化到 DTO 或 struct 时就可能失败。
这时问题不在 JSON 语法,而在 schema 不匹配。
排查方向应该变成:
- 字段名是否一致
- 字段类型是否一致
- 必填字段是否缺失
- 数组和对象是否写反
- 空值是否允许
- 枚举值是否在合法范围内
例如接口约定是:
json
{
"tags": ["frontend", "json"]
}
但实际传成:
json
{
"tags": "frontend,json"
}
这不是语法错误,但依然会导致业务解析失败。
不报错但更危险:重复 key
重复 key 通常不会让 JSON 解析器报错,但会带来隐蔽问题。
例如:
json
{
"role": "user",
"role": "admin"
}
很多解析器会保留最后一个值,结果变成:
json
{
"role": "admin"
}
不同语言、不同库对重复 key 的处理不一定完全一致。对配置、权限、签名、风控字段来说,这类问题尤其危险。
如果 JSON 来自外部输入,建议在 schema 校验或解析层增加重复 key 检测,而不是只依赖默认解析结果。
大整数精度:解析成功也可能已经错了
JavaScript 里的 number 使用双精度浮点数,安全整数范围有限。
比如:
js
JSON.parse('{"id": 9007199254740993}')
解析后可能已经丢失精度。
对于订单号、雪花 ID、数据库主键这类大整数,不建议作为数字传给前端,最好改成字符串:
json
{
"id": "9007199254740993"
}
这类问题不会表现为 JSON 解析失败,但会在后续查询、比对、跳转详情页时出错。看到长 ID 时,要特别注意它是不是应该用字符串承载。
我在线上会怎么排查
实际排查时,可以按这个顺序走:
第一步,看 HTTP 状态码。
如果是 401、403、404、500,优先查鉴权、路由、网关、服务端异常,不要先查 JSON 格式。
第二步,看 Content-Type。
如果不是 application/json 或约定的 JSON 类型,要确认是否拿到了 HTML、纯文本、下载文件或错误页。
第三步,看响应体前 200 个字符。
开头是 <,多半是 HTML;开头为空,先查空响应;开头是 { 或 [,再继续看 JSON 结构。
第四步,把原始文本单独校验。
如果只是想快速确认一段文本是否符合严格 JSON 语法,可以用在线校验器辅助检查,例如 JSON Toolbox:jsontoolbox.cn 。但线上问题最终还是要以接口响应、日志和服务端返回为准,在线工具只适合做临时验证。
第五步,对照接口 schema。
JSON 语法合法之后,再看字段类型、必填字段、数组对象结构、枚举值和空值约定。
常见错误速查表
| 报错或现象 | 常见原因 | 排查重点 |
|---|---|---|
Unexpected token < |
返回了 HTML | 看响应体开头、状态码、接口路径、登录态 |
Unexpected end of JSON input |
空响应或内容截断 | 看响应体长度、204、网关超时、服务端异常 |
Expecting property name |
key 没加双引号或用了 JS 对象写法 | 检查双引号、尾逗号、注释、单引号 |
Extra data |
多个 JSON 文档拼在一起 | 区分 JSON 数组和 NDJSON |
Invalid escape |
字符串转义错误 | 检查反斜杠、换行、路径、正则 |
| 解析成功但业务失败 | schema 不匹配 | 检查字段名、类型、必填、数组对象结构 |
| 解析成功但值不对 | 大整数精度丢失 | 长 ID 改成字符串 |
| 权限字段异常 | 重复 key 被覆盖 | 增加重复 key 检测 |
总结
JSON 解析失败时,最重要的不是记住每一种报错文案,而是先确认输入。
一个比较可靠的判断顺序是:
text
状态码 -> Content-Type -> 原始响应体 -> JSON 语法 -> 业务 schema -> 精度和重复 key
很多问题走到第三步就能定位:拿到的不是 JSON、响应为空、响应被截断、或者后端返回了 HTML 错误页。
只有确认输入确实是 JSON 之后,才需要继续检查双引号、尾逗号、转义、数组对象结构和字段类型。这样排查会比直接盯着解析异常更快,也更容易定位到真正的责任边界。