AI 生成的 JSON 为什么会出现 JSON.parse() 解析错误?完整原因与解决方法

截至 2026 年 9 月 17 日:把聊天回复直接丢进 JSON.parse(),失败通常不是「模型不会 JSON」,而是 Markdown 围栏、前后文、尾逗号、截断与 JS 方言。原因分类与修复顺序。

先给结论:JSON.parse() 报错,多半不是「模型不会写 JSON」,而是你把一整段聊天回复当成了 JSON 文本。JSON.parse 只收一份符合 JSON 文法(ECMA-262 / RFC 8259)的值。Markdown 围栏、前后解释、尾逗号、未转义换行、截断、JS / Python 方言,都会让它立刻抛 SyntaxError。2026 年的修法顺序是:能开 Structured Output 或读 Tool Calling 的 arguments 字段,就不要解析聊天正文;必须解析时,先提取再 parse,成功后再用 JSON Schema 校验——不要一上来用正则「修到能 parse」。

这篇按 2026 年 9 月 17 日写。本站已有《Structured Output 是什么》《从 Prompt 到 Structured Output》《OpenAI / Gemini Structured Output 对比》《Gemini API 结构化 JSON》《Tool Calling 与 JSON Schema》。本文只回答:模型输出为什么过不了 JSON.parse,以及该按什么顺序修。

JSON.parse 到底收什么

浏览器和 Node 里的 JSON.parse 实现的是 JSON 文本,不是「看起来像对象字面量的 JavaScript」。空白(空格、制表、换行、回车)可以出现在值的两侧;除此之外,输入必须是恰好一份值:对象、数组、字符串、数字、true / false / null。值后面再跟非空白,就会报错——Chrome 常写成 Unexpected non-whitespace character after JSON。

下面这些在 JS 里能跑、在 JSON 里不行。模型经常从训练语料里把它们一并写出来:

写法JavaScript 对象 / JSON5JSON.parse
尾逗号{"ok": true,} 可以抛错
单引号{'ok': true} 可以抛错
注释// note 可以抛错
未加引号的键{ok: true} 可以抛错
undefined / NaN / Infinity语言里有抛错
字符串里的裸换行模板字符串可以抛错,必须写成 \n

所以排障时先问一句:你喂给 JSON.parse 的,是「一份 JSON 值」,还是「模型为了好看包过的一段话」?前者才是解析器的职责;后者要先提取。

一张表:失败原因分类

把线上遇到的 SyntaxError 先归类,比对着报错原文死磕更快。同一条报错在 Chrome、Safari、Node 里用词不同,但原因就这几类:

类别模型常写出什么典型后果该先做什么
包装层```json 围栏、句首「如下所示」第一个字符就不是 { / [剥围栏,再取平衡的值
语法方言尾逗号、单引号、注释、裸键名Unexpected token换 Structured Output;不要当 JS 解析
字符串损坏未转义 "、裸换行、全角逗号字符串提前结束或键后不是 :看出错列;限制字段长度
截断对象没闭合、数组缺 ]Unexpected end of JSON input加大输出上限;等流结束
多值两段 JSON、JSON 后面跟解释第一个值之后还有字符只切第一份完整值
编码BOM、零宽字符、二次 stringify怪符号或 parse 出字符串去 BOM;判断类型后再 parse

Agent 场景还要多记一条:Tool Calling 的 arguments 往往已经是对象或一段由厂商保证的 JSON 字符串,不要再把整条 assistant 消息丢进 JSON.parse。那是另一条通道,见《Tool Calling 为什么依赖 JSON Schema》。

围栏与前后文:最常见的一层皮

聊天模型被训练成「把代码放进围栏」。你即使写了「只输出 JSON」,回复仍经常是:

```json
{"ok": true, "id": "A-1024"}
```
以上是结果,需要的话我可以再解释字段。

这份文本的第一个字符是反引号,不是 {。JSON.parse 会在第一列失败。句首加「好的,JSON 如下:」、句尾加免责声明,同理。更隐蔽的是两份值:先给一份「示例」,再给一份「真正结果」——解析器若吃整段,会在第一份 } 之后炸掉。

提取原则就一句:找出第一对平衡的 {} 或 [](要跳过字符串里的括号),只把这一段交给 JSON.parse。有围栏就先剥围栏。不要用「从第一个 { 切到最后一个 }」这种贪心切片——字符串里的括号、后面跟着的解释对象,都会切错。

语法方言:尾逗号、单引号、注释、未加引号的键

模型见过大量 JavaScript、Python、JSON5、YAML。生成「结构化数据」时,它常混用这些方言。对 JSON.parse 来说,它们全部非法:

{
  ok: true,          // 裸键 + 注释
  'name': 'Ada',     // 单引号
  "tags": ["a",],    // 尾逗号
  "flag": True       // Python 布尔
}

还有 undefined、NaN、Infinity、None。它们在各自语言里有意义,JSON 只有 null 和有限数字。把 JSON.parse 换成 eval 或 new Function 来「兼容」这些写法,会把解析器变成任意代码执行口,生产里不要这样做。

JSON5、JSONC 可以吃注释和尾逗号,适合人类手写配置,不适合当模型输出的默认解析器。方言一开,你就不再能区分「模型多写了一个逗号」和「模型写坏了字符串」。要宽松,只在提取失败后的修复层用,而且修完仍要走 Schema。

字符串与标点:转义、换行、全角与智能引号

合法 JSON 的字符串必须用双引号包起来,内部的 " 和反斜杠必须转义,控制字符必须写成 \n、\t 或 \uXXXX。模型摘要一段用户评论时,原文里的引号和换行经常原样掉进字段,于是字符串提前结束,后面的中文或逗号变成「意外的 token」。

中文场景还有一套高频脏字符:全角逗号 ,、全角冒号 :、弯引号 “” / ‘’。它们看起来像标点,码点不是 0x2C / 0x3A / 0x22。下面这份「像 JSON」的文本,会在 name 的值后面炸掉:

{
  "name": "张三",
  "ok": true
}

防御不靠「再写一句请用半角标点」。把长文本字段写进 Schema 的 maxLength,抽取时让模型引用原文而不是手打标点,最终通道用 Structured Output。排障时把原文贴进 JSON 校验,看高亮停在哪一列——全角逗号一目了然。

截断与流式:Unexpected end of JSON input

Unexpected end of JSON input 几乎总是文本在文法结束前就断了:对象缺 },数组缺 ],字符串缺收尾引号。2026 年常见来源有三条:输出 token 上限;安全过滤中途切断;你在流式响应里对不完整 chunk 调用了 JSON.parse。

流式接口每次只给你增量。前几个 chunk 可能是 {"ok": tr,此时 parse 必然失败。正确做法:

  • 等流结束(finish_reason / stop)再 parse 完整缓冲区;
  • 或者用真正的流式 JSON 解析器,按 token 推进,不要对半截文本调用 JSON.parse;
  • 若结束原因是 length / max_tokens,这不是解析问题,是生成没写完——加大上限、缩小 Schema、或让模型分页。

截断后用「自动补 }」去猜闭合,只适合草稿。补出来的结构可能缺字段、截断字符串,看起来能 parse,业务是错的。补完必须 Schema 校验,失败就重试,不要默默入库。

隐身字符与二次编码

UTF-8 BOM(U+FEFF)不是 JSON 空白。某些复制路径、部分网关会在正文前加 BOM,JSON.parse 会在第 0 列报意外 token。零宽空格、软连字符也一样。提取前先 replace(/^\uFEFF/, ""),再 trim。

二次编码更隐蔽。你 JSON.stringify 过一次,得到字符串 "{\"ok\":true}";如果把带外层引号的那份再 parse,得到的是字符串 {"ok":true},不是对象。再 parse 一次才得到对象。反过来:只 parse 一次就当对象用,后面 .ok 是 undefined,看起来像「解析成功但没字段」。排障时先 typeof,再决定要不要二次 parse。不要写死「parse 两遍」——遇到真正的对象会炸。

修复顺序:先换通道,再提取,最后才修

按这个顺序做,比堆提示词稳:

  1. 换通道。最终答复走 Structured Output(OpenAI response_format.json_schema,Gemini responseMimeType + Schema,Claude output_config.format)。工具参数走 Tool Calling 的 arguments,不要从散文里抠。原理见《Structured Output 是什么》。
  2. 提取。剥 ```json 围栏;按括号平衡切出第一份值;去掉 BOM。
  3. 严格 parse。只用 JSON.parse。失败就记原始文本与报错位置,不要 eval。
  4. Schema 校验。parse 成功只说明文法对。缺字段、类型错、多出来的键,要靠 JSON Schema / ajv。见《从 Prompt 到 Structured Output》。
  5. 修复是最后一层。jsonrepair 一类工具可以补尾逗号、补闭合括号。只在提取 + parse 失败、且你接受「修过的文本可能改语义」时用。修完仍要走 3 和 4。不要把它设成全局默认解析器。

提示词仍然有用:「不要围栏、不要解释」。它降低包装层出现的概率,不能代替 Schema,也不能让 JSON.parse 变宽松。2026 年还把聊天正文当 API 的,会在围栏和截断上反复付费。

一段可落地的提取 + parse

下面是教学用的最小流水线:剥围栏、去掉 BOM、按括号平衡切片,再 JSON.parse。它处理常见包装,不尝试修尾逗号或全角标点——那些应留给 Structured Output 或显式修复层。

function stripFence(text) {
  const m = String(text).match(/```(?:json|JSON)?\s*([\s\S]*?)```/);
  return m ? m[1] : String(text);
}

function sliceBalancedJson(text) {
  const src = text.replace(/^\uFEFF/, "").trim();
  const start = src.search(/[\{\[]/);
  if (start < 0) throw new SyntaxError("No JSON value found");
  const open = src[start];
  const close = open === "{" ? "}" : "]";
  let depth = 0, inStr = false, esc = false;
  for (let i = start; i < src.length; i++) {
    const ch = src[i];
    if (inStr) {
      if (esc) { esc = false; continue; }
      if (ch === "\\") { esc = true; continue; }
      if (ch === '"') inStr = false;
      continue;
    }
    if (ch === '"') { inStr = true; continue; }
    if (ch === open) depth++;
    else if (ch === close) {
      depth--;
      if (depth === 0) return src.slice(start, i + 1);
    }
  }
  throw new SyntaxError("Unterminated JSON value");
}

function parseModelJson(raw) {
  return JSON.parse(sliceBalancedJson(stripFence(raw)));
}

切片器必须跟踪「是否在字符串里」,否则字段值里的 { 会提前闭合。嵌套对象、数组同理,靠 depth。若切片成功仍 parse 失败,把失败文本贴进校验工具,对照上一节的分类表,不要在这一层继续加正则。

用本地 JSON 工具看报错出在哪

模型输出别先丢进生产解析器。先在浏览器里看三件事:它是不是合法 JSON;非法时停在哪一列;若你已经有 Schema,它过不过合同。

  • JSON 校验 — 看 SyntaxError 的位置;有 Schema 就一并校验字段。
  • JSON 格式化 — 能格式化,通常就能 parse;格式化失败,对照原文找全角逗号或围栏。
  • JSON Diff — parse 成功之后,对比「模型对象」和「你允许的最小对象」。

数据不离开浏览器。适合把一段失败的模型回复、一份 Schema、一次 Tool Calling arguments 放在一起看。字段名和 required 稳定了,再接到 Host。

常见问题 FAQ

为什么「看起来是 JSON」还会被 JSON.parse 拒绝?

人眼容忍围栏、尾逗号、弯引号和解释性前后文。JSON.parse 按 RFC 8259 收恰好一份值。像,不等于合法。

用正则删掉 ```json 围栏就够了吗?

不够。围栏只是包装层之一。后面还有解释句子、第二份 JSON、尾逗号和截断。剥围栏之后仍要做平衡切片和严格 parse。

JSON Mode 和 Structured Output 有什么差别?

JSON Mode 多半只约束「像 JSON」,不管字段和类型。Structured Output 用 JSON Schema 在解码阶段挡非法 token。要进程序,优先 Structured Output,不要只开 JSON Mode 再 JSON.parse 聊天正文。

该把 jsonrepair 或 JSON5 当成默认解析器吗?

不该。它们会吞下本该失败的输入,语义可能被改掉。只在提取 + JSON.parse 失败后当修复层,修完仍要 Schema 校验。

流式输出时什么时候才能调用 JSON.parse?

等流结束、缓冲区是完整值之后。对半截 chunk 调用 JSON.parse,会稳定得到 Unexpected end of JSON input。要边到边消费,用流式解析器,不要用 JSON.parse。

解析成功但字段不对,算这篇文章的范围吗?

不算同一层。JSON.parse 只保证文法。缺字段、类型错、多余键是 Schema 问题,见本站 Structured Output 与 Tool Calling 校验两篇。

总结

JSON.parse 失败,是「通道」问题,不是「再写一句请输出 JSON」能解决的问题。聊天模型会包围栏、混方言、在 token 上限处截断;解析器只收一份干净的 JSON 值。2026 年把模型接到程序,正确顺序是 Structured Output / Tool Calling arguments,其次是提取 + 严格 parse + Schema,最后才是修复器。

提示词可以少制造包装层,不能放宽文法。先在本地校验工具里看失败文本停在哪一列,再决定是剥围栏、换通道,还是加大输出上限。模型会变;JSON.parse 收什么、你的字段合同是什么,不应跟着变。