← 全部文章

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 配置。
TFN 或其他标识符 输入里用了 TrueFalseNoneNaNInfinityundefined 等非 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 是 TFNI 或其他标识符

严格 JSON 只有三个字面量单词:truefalsenull,而且都是小写。

下面这些都不是 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,拿到之后照样要校验。
  • 遇到 NaNInfinity,得先决定这个值该变成 null、某个字符串哨兵值,还是抛一个领域相关的错误。

JSON.stringify() 会把对象值里的 NaNInfinity-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.okcontent-type,把接口问题解决了再去解析。

怎么修 "Unexpected token u in JSON at position 0"?

你把 undefined 或一个没赋值的变量传给了 JSON.parse()。解析前先做防护,并且明确地初始化状态、函数参数、localStorage 的兜底值或环境变量。

为什么 JSON.parse({}) 会报 token o 或 [object Object]?

JSON.parse() 要的是文本。普通对象会被强制转成字符串 [object Object],于是解析器在 objecto 上报错。直接用这个对象;确实需要 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 指南

参考资料

最后校订于 2026 年 8 月。