JSON 解析错误全解:Unexpected token、输入提前结束、逗号、引号与 HTML
按你实际拿到的输入来定位 JSON.parse 错误:HTML、undefined、对象、尾随逗号、错误转义、控制字符、被截断的 JSON,以及合法文档后面多出来的数据。
JSON.parse() 的严格是有意为之的。它不解析"近似 JSON"、不解析 JavaScript 对象字面量、不解析 JSONC 配置、不解析 HTML 报错页,也不解析那种"本来是个对象、后来被谁转成了 "[object Object]""的值。
"Unexpected token" 错误说明解析器在 JSON 语法的某个位置上,看到了一个不该出现的字符。token 告诉你解析器实际拿到了什么,位置(position、行、列)告诉你该去哪儿看。先把这两条事实弄清楚,再去猜 API 或者换解析器。
先看 token
把第一个意外 token 当成捷径。在真实项目里,从头到尾读一遍载荷,找到 bug 的速度远不如查下面这张表。
| Token 或报错信息 | 通常意味着 | 先查哪里 |
|---|---|---|
< |
响应是 HTML,不是 JSON。通常是 404 页、登录跳转、代理报错,或者 SPA 的兜底路由。 | Network 面板、状态码、content-type、请求 URL、鉴权 Cookie。 |
位置 0 的 u |
你传进去的是 undefined 或一个没赋值的变量。 |
函数参数、状态初始化、环境变量、可选的缓存读取。 |
位置 1 的 o |
一个普通对象被强制转成了 "[object Object]"。 |
对对象调用 JSON.parse() 的代码,或者写成 body: payload 而不是 JSON.stringify(payload) 的地方。 |
' |
输入用的是单引号。 | 拷来的 JavaScript 对象字面量、Python 的 str(dict)、手工改过的配置。 |
逗号后面的 } 或 ] |
输入里有尾随逗号。 | 对象或数组的最后一项。 |
/ |
输入里有 // 或 /* */ 注释。 |
被丢进严格 JSON 解析器的 JSONC 配置。 |
T、F、N 或其他标识符 |
输入里用了 True、False、None、NaN、Infinity、undefined 等非 JSON 字面量。 |
Python 输出、JavaScript 调试输出、大模型生成的片段。 |
| 位置 0 的不可见字符 | 第一个可见字符前面藏着 BOM 或控制字节。 | 文件编码、复制来的文本、命令行输出。 |
| Unexpected end of JSON input | 对象、数组或字符串还没闭合,文本就没了。 | 空响应、被截断的流、缺失的括号。见输入提前结束。 |
| JSON 数据后面出现非空白字符 | 一个完整 JSON 值后面还有多余文本。 | 日志和 JSON 混在一起、两份 JSON 文档被拼接、把 NDJSON 当成单个值来解析。 |
不同引擎的措辞各不相同:
SyntaxError: Unexpected token '<', "<html>..." is not valid JSON
SyntaxError: Unexpected token u in JSON at position 0
SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data
SyntaxError: JSON Parse error: Single quotes (') are not allowed in JSON
别死抠具体措辞。Chrome、Node.js、Firefox 和 Safari 用的不是同一套错误信息。去看输入,把报错当提示就行。
把你真正解析的东西打出来
调 JSON 问题浪费掉的时间,多半花在"想当然地认为输入就是我要解析的那个值"上。动手修之前,先打一条安全的预览日志。
function previewForJsonParse(value) {
const text = typeof value === 'string' ? value : String(value);
return {
type: typeof value,
length: text.length,
firstChar: JSON.stringify(text[0] ?? ''),
preview: JSON.stringify(text.slice(0, 160)),
};
}
console.log(previewForJsonParse(raw));
这条预览能立刻暴露好几类问题:
type: "undefined"说明你手上压根没有 JSON 文本。preview: "\"[object Object]\""说明解析之前对象已经被强制转成字符串了。preview: "\"<!DOCTYPE html>...\""说明你的 API 返回了 HTML。firstChar: "\"\\ufeff\""说明位置 0 上有个 BOM。length: 0说明你在解析一个空字符串,而不是什么格式错误的对象。
生产代码里,与其让某个组件随机崩掉,不如返回一个解析结果:
function safeJsonParse(text) {
try {
return { ok: true, value: JSON.parse(text) };
} catch (error) {
return {
ok: false,
error: error instanceof SyntaxError ? error.message : String(error),
};
}
}
这个模式适合用在应用的边缘:表单导入、localStorage、功能开关、剪贴板粘贴、开发者工具。而对内部的 API 约定,通常更好的做法是让它响亮地失败,然后去修生产方。
token 是 <:你的 fetch 返回了 HTML
Unexpected token '<' 是 fetch 最经典的翻车方式。响应体以 < 开头,因为服务端返回的是一份 HTML 文档:
/api/user拼错了,返回了应用的外壳页面。- 用户已登出,拿到的是登录页。
- 反向代理返回了一个带品牌样式的 502 页面。
- Serverless 路由崩了,返回 HTML 错误页。
- 静态托管把 API 路径重写到了
index.html。
下面这个辅助函数只读一次响应体,检查状态码、检查内容类型,并在错误里附上一小段预览:
async function readJsonResponse(response) {
const text = await response.text();
const contentType = response.headers.get('content-type') || '';
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`);
}
if (!contentType.includes('application/json')) {
throw new Error(
`Expected JSON, got ${contentType || 'no content-type'}: ${text.slice(0, 200)}`,
);
}
try {
return JSON.parse(text);
} catch (error) {
throw new Error(
`Invalid JSON response: ${error.message}. Body starts with ${JSON.stringify(
text.slice(0, 120),
)}`,
);
}
}
调试这类问题时,不要一上来就 await response.json()。流一旦被消费,除非你克隆了 response,否则原始响应体就拿不回来了。先读成文本,看一眼,再解析。
token 是 u:你解析了 undefined
JSON.parse(undefined) 收到的并不是一个特殊的 JavaScript 值。算法会先把参数转成字符串,于是解析器看到的是 "undefined",并在 u 上报错。
JSON.parse(undefined);
// SyntaxError: Unexpected token u in JSON at position 0
常见原因:
- 某个函数参数本来是可选的,但解析处默认它一定存在。
- React/Vue/Svelte 组件在异步加载完成之前就去解析数据了。
- 环境变量没配。
- 缓存封装在未命中时返回了
undefined。 - 属性路径写错了:写成
settings.userJson,实际是settings.user.json。
要防的是数据来源,不是解析器:
function readConfig(raw) {
if (typeof raw !== 'string' || raw.trim() === '') {
return { theme: 'system', compact: false };
}
return JSON.parse(raw);
}
const result = readConfig(process.env.APP_SETTINGS_JSON);
有个细节常让人意外:JSON.parse(null) 返回 null。因为 null 会被转成字符串 "null",而 "null" 是合法 JSON。undefined 则不是。
token 是 o:你解析了一个对象
JSON.parse() 要的是文本。你传一个普通对象进去,JavaScript 会把它强制转成 "[object Object]"。解析器把开头的 [ 当成数组的起始,接着就在位置 1 的 o 上报错。
const payload = { name: 'Ada' };
JSON.parse(payload);
// SyntaxError: Unexpected token o in JSON at position 1
已经拿到对象了,那就把这次解析删掉:
const payload = { name: 'Ada' };
// 本来就是对象,直接用。
console.log(payload.name);
发请求时,先序列化再发:
await fetch('/api/profile', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: 'Ada' }),
});
如果你的日志里躺着的就是字面量 "[object Object]",那修复工具也救不回原来的键和值 —— 数据在更早的地方就丢了。去修产生这个字符串的那条代码路径。关于序列化边界的更多讨论,见如何 stringify JSON。
token 是引号、逗号或斜杠:你手上是"近似 JSON"
很多 unexpected token 错误,都来自那些"在别处合法、只是在严格 JSON 里不合法"的语法。
单引号在 JavaScript 字符串里合法,在 JSON 字符串里不合法:
JSON.parse("{'name': 'Ada'}");
// SyntaxError
JSON.parse('{"name": "Ada"}');
// OK
不加引号的键在 JavaScript 对象字面量里合法,在 JSON 里不合法:
JSON.parse('{name: "Ada"}');
// SyntaxError
JSON.parse('{"name": "Ada"}');
// OK
尾随逗号在现代 JavaScript 和很多配置格式里合法,在 JSON 里不合法:
JSON.parse('{"name": "Ada", "score": 98,}');
// 在闭合大括号附近 SyntaxError
JSON.parse('{"name": "Ada", "score": 98}');
// OK
注释在 JSONC 里合法,在 JSON 里不合法:
JSON.parse(`{
// 内部备注
"name": "Ada"
}`);
// 在 / 附近 SyntaxError
别用那种随手写的正则去删注释,除非它能识别字符串边界。下面这种写法会毁掉真实数据:
const raw = '{"url": "https://example.com/a//b"}';
// 馊主意:这会把字符串值的一部分删掉。
raw.replace(/\/\/.*$/gm, '');
配置文件如果本来就打算用 JSONC,那就正经用 JSONC 解析器。对于粘贴进来的、一次性的输入,先修好、再按严格 JSON 校验,最后检查输出。
token 是 T、F、N、I 或其他标识符
严格 JSON 只有三个字面量单词:true、false、null,而且都是小写。
下面这些都不是 JSON:
JSON.parse('{"enabled": True}');
JSON.parse('{"enabled": False}');
JSON.parse('{"value": None}');
JSON.parse('{"value": undefined}');
JSON.parse('{"ratio": NaN}');
JSON.parse('{"limit": Infinity}');
能修生产方就修生产方:
- Python 里用
json.dumps(data),别用str(data)。 - JavaScript 里用
JSON.stringify(data),别用模板字符串拼接对象。 - 大模型输出,要求它返回严格 JSON,拿到之后照样要校验。
- 遇到
NaN和Infinity,得先决定这个值该变成null、某个字符串哨兵值,还是抛一个领域相关的错误。
JSON.stringify() 会把对象值里的 NaN、Infinity、-Infinity 转成 null。画图表时这也许可以接受,但它是一个有损的决定。在财务、权限、配额和审计日志里,绝不能让它悄悄发生。
位置 0 看着没毛病:检查不可见字符
有时候第一个可见字符明明是 {,解析器却报位置 0 有意外 token。别信眼睛,去看字符码。
function inspectFirstCharacter(text) {
const char = text[0] ?? '';
return {
visible: JSON.stringify(char),
codePoint: char ? `U+${char.codePointAt(0).toString(16).toUpperCase()}` : null,
};
}
UTF-8 的 BOM 会显示成 U+FEFF。只在文档开头把它去掉:
const clean = raw.replace(/^\uFEFF/, '');
const data = JSON.parse(clean);
不要无差别地删掉所有控制字符。制表符、换行和回车是 JSON token 之间合法的空白,而字符串内部转义过的 \n 之类是有意义的。下一节会把原始控制字符、坏掉的转义和未闭合的字符串分开讲。
把 position 换算成行和列
V8 风格的报错通常带一个 position 123。这个位置是从 0 开始的字符下标。把 bug 转给别人之前,先把它换算成行号和列号。
function describeJsonParseError(text, message) {
const match = String(message).match(/position (\d+)/);
if (!match) return null;
const position = Number(match[1]);
const before = text.slice(0, position);
const line = before.split('\n').length;
const column = before.length - before.lastIndexOf('\n');
return {
position,
line,
column,
character: JSON.stringify(text[position] ?? ''),
context: JSON.stringify(text.slice(Math.max(0, position - 30), position + 30)),
};
}
打日志时,把字符和上下文用 JSON.stringify() 包一层 —— 这样引号、制表符、换行、BOM 这些看不见的细节才会现形。
解析提前结束:检查空输入和被截断的输入
Unexpected end of JSON input 意思是解析器还需要下一个 token,字符串却已经到头了。缺的往往是闭合的 }、] 或引号,但输入也可能压根就是空的。
JSON.parse(''); // 空输入
JSON.parse('{"user":{"id":7}'); // 缺 }
JSON.parse('{"name":"Ada}'); // 缺闭合引号
在 fetch 代码里,空响应体可能是完全正常的。204 No Content、某些 304 响应,以及那些故意不返回任何表示的接口,都不该被交给 response.json()。
async function readJsonOrNull(response) {
if (response.status === 204 || response.status === 205) return null;
const text = await response.text();
if (!text.trim()) return null;
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${text.slice(0, 160)}`);
}
return JSON.parse(text);
}
如果一个非空响应在某个值中间就断了,别顺手补个括号了事 —— 传输被截断,很可能连字段或数组元素都一起丢了。有 content-length 就比对一下,翻翻代理和服务端日志,然后重试请求。读流的时候,要么把一份完整 JSON 文档缓冲齐,要么用明确的分帧格式;随意切分的网络分片不是能解析的文档。
尾随逗号在 JSON 里不合法
尾随逗号,指的是紧挨在闭合大括号或方括号前面的逗号:
{
"name": "Ada",
"roles": ["admin", "editor",]
}
这里有两处错误:"editor" 后面那个逗号,以及假如再有一个对象属性、它后面本会跟上的那个逗号。JavaScript 的对象和数组字面量允许尾随逗号,JSON 不允许。稳妥的改法是结构性的:
{
"name": "Ada",
"roles": ["admin", "editor"]
}
生产环境里别用全局的 /,\s*([}\]])/ 去替换。它可能改写字符串内部长得像逗号的文本,而且会把生产方的毛病盖住。粘贴进来的输入,用能识别 JSON 结构的修复工具;生成的文件,用 JSON.stringify()、标准库序列化器,再在 CI 里加一道严格解析检查。
区分坏转义、控制字符和未闭合字符串
这几类报错都指向 JSON 字符串内部,但它们描述的是不同的毛病:
| 错误类型 | 非法示例 | 正确写法 |
|---|---|---|
| 坏掉的转义字符 | {"path":"C:\new\q"} |
转义反斜杠:{"path":"C:\\new\\q"} |
| 坏掉的 Unicode 转义 | {"mark":"\u12G4"} |
\u 后面必须正好跟四位十六进制数字 |
| 原始控制字符 | 引号里夹着真实的换行或制表符 | JSON 文本里要写成 \n 或 \t |
| 未闭合的字符串 | {"name":"Ada} |
确认值没被截断之后,再补上缺的引号 |
JSON 认可的转义是 \"、\\、\/、\b、\f、\n、\r、\t 和 \uXXXX。\q 这种未知序列不合法。带引号的 JSON 字符串里也不允许出现真实的换行 —— 尽管转义后的 \n 解析出来就是同一个字符。
当 JSON 被嵌在 JavaScript 字符串里时,记住有两层语法都在吃转义。下面这段 JavaScript 源码需要双写反斜杠,才能把一个 JSON 转义真正交给 JSON.parse():
const text = '{"message":"first\\nsecond"}';
console.log(JSON.parse(text).message);
如果字符串来自失败的传输或被截断的日志,修复工具没法判断缺的是什么内容。不要凭空补一个闭合引号 —— 要么拒绝它,要么把完整数据重新取回来。
合法 JSON 后面还跟着别的数据
unexpected non-whitespace character after JSON data 这类报错,意思是解析器顺利读完了一个值,然后又撞见了另一个字符:
{"id":1}{"id":2}
{"id":1} request completed
不要把第一个闭合大括号之后的东西全丢掉。输入可能是两条拼在一起的记录、JSON 和日志混排,也可能是 NDJSON(每行一个 JSON 值)。NDJSON 要按记录逐条解析:
function parseNdjson(text) {
return text
.split(/\r?\n/)
.filter((line) => line.trim() !== '')
.map((line, index) => {
try {
return JSON.parse(line);
} catch (error) {
throw new Error(`Invalid NDJSON record ${index + 1}: ${error.message}`);
}
});
}
流式协议要用真正的边界:换行分隔的 JSON、长度前缀,或者其他有明确文档的分帧方式。没有分隔符、直接拼在一起的对象是有歧义的,应该在生产方那边修掉。
决定是修,还是拒
不是所有 unexpected token 都该被"修正"。当输入是人写的、或者处于探索阶段时,JSON 修复很有帮助;而当输入代表一份约定、一项权限、一笔支付或一次破坏性操作时,修复是危险的。
| 场景 | 合适的做法 |
|---|---|
| 用户把大致像 JSON 的东西粘进了开发者工具 | 修复、格式化,然后把结果摆出来让人复核。 |
| 大模型返回了近似 JSON | 只把修复当清理步骤,之后仍要校验必需的键和类型。 |
| 你自己的 API 返回了格式错误的 JSON | 拒绝这个响应,去修服务端或序列化器。 |
| fetch 返回了 HTML | 修 URL、鉴权、路由、代理或 content type。别试图把 HTML "修"成 JSON。 |
| 仓库里的配置文件带注释或尾随逗号 | 要么明确按 JSONC 用,要么在 CI 里用 jq empty file.json 强制严格 JSON。 |
| 财务、权限、迁移或删除流程收到了非法 JSON | 拒绝它,把原始预览记进日志,并要求生产方给出合法数据。 |
正是这条界线,让修复工具既好用,又不至于把上游的 bug 藏起来。
粘贴进来的输入,用 JSON Fix
如果你手上有一段坏掉的 JSON 样本,想快点拿到干净版本,把它粘进 JSON Fix。它能修常见语法问题:单引号、尾随逗号、没加引号的键、注释、Python 字面量、undefined、Markdown 围栏和未闭合的结构。修复在你的浏览器里运行,文本不用离开你的机器。
修完之后,再走一遍:
- 格式化输出,确认结构没跑偏。
- 数据要紧的话,用 JSON Diff 比对修复前后。
- 用严格 JSON 解析校验最终文本。
- API 载荷、导入数据和配置,还要过一遍 schema 校验。
常见问题
"Unexpected token in JSON" 是什么意思?
JSON.parse() 在 JSON 语法的那个位置上遇到了不合法的字符。报出来的 token 告诉你解析器实际收到了什么,位置告诉你该去输入的哪一段检查。
用 fetch 时为什么会出现 "Unexpected token <"?
响应体以 HTML 开头,通常是 404 页、登录跳转、代理报错,或者前端的兜底路由。先把响应读成文本,检查 response.ok 和 content-type,把接口问题解决了再去解析。
怎么修 "Unexpected token u in JSON at position 0"?
你把 undefined 或一个没赋值的变量传给了 JSON.parse()。解析前先做防护,并且明确地初始化状态、函数参数、localStorage 的兜底值或环境变量。
为什么 JSON.parse({}) 会报 token o 或 [object Object]?
JSON.parse() 要的是文本。普通对象会被强制转成字符串 [object Object],于是解析器在 object 的 o 上报错。直接用这个对象;确实需要 JSON 文本时,调用 JSON.stringify()。
怎么精确定位出错位置?
如果引擎报了 position,就按这个下标切分字符串,换算成行号和列号。打日志时把片段字符串化,好让不可见字符和引号都显形。
JSON.parse() 能处理注释或尾随逗号吗?
不能。注释和尾随逗号在某些 JavaScript 和 JSONC 场景里合法,在严格 JSON 里不合法。要么删掉它们,要么给配置文件换上 JSONC 解析器 —— 总之在严格解析之前先把输入修好。
该不该自动修复 unexpected token 错误?
粘贴的示例、开发者工具和大模型输出,在人工复核的前提下可以修。而 API 约定、财务操作、权限变更或破坏性流程,请直接拒绝非法 JSON,然后去修生产方。
为什么 Chrome、Firefox 和 Safari 报的 JSON.parse 错误不一样?
它们用的是不同的 JavaScript 引擎,措辞也各不相同。所以别抠字面:先确认这份载荷到底是不是 JSON,再从实际的 token 入手,把报错文本当线索,配合输入预览和行列号来定位。
"Unexpected end of JSON input" 是什么原因?
值还没读完,输入就没了。去找这几种情况:空响应;缺失的闭合大括号、方括号或引号;传输被截断;或者代码在完整文档到达之前就去解析流了。
未闭合字符串或坏掉的转义字符怎么修?
到报错位置附近去看源文本:先确认这个值没有被截断,再把反斜杠和控制字符正确转义,最后补上结尾引号。机器生成的数据,请换一个靠谱的序列化器,而不是拿正则去打补丁。
为什么合法 JSON 后面还有多余数据?
可能是拼接在一起的多份文档、JSON 混着日志文本,也可能是 NDJSON。先搞清楚分帧格式,再逐条认真解析,别把剩下的部分默默扔掉。
相关 JSON 指南
参考资料
- RFC 8259 —— JSON 数据交换格式与语法规范。
- MDN: JSON.parse() —— JavaScript 解析器的行为与异常。
- ECMA-262 JSON.parse —— 语言层面的算法,包括输入的强制转换。
- MDN: Response.json() —— fetch 响应的解析行为。
- NDJSON 规范 —— 换行分隔的 JSON 记录分帧格式。
最后校订于 2026 年 8 月。