先给结论: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 对象 / JSON5 | JSON.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 两遍」——遇到真正的对象会炸。
修复顺序:先换通道,再提取,最后才修
按这个顺序做,比堆提示词稳:
- 换通道。最终答复走 Structured Output(OpenAI
response_format.json_schema,GeminiresponseMimeType+ Schema,Claudeoutput_config.format)。工具参数走 Tool Calling 的 arguments,不要从散文里抠。原理见《Structured Output 是什么》。 - 提取。剥
```json围栏;按括号平衡切出第一份值;去掉 BOM。 - 严格 parse。只用
JSON.parse。失败就记原始文本与报错位置,不要eval。 - Schema 校验。parse 成功只说明文法对。缺字段、类型错、多出来的键,要靠 JSON Schema / ajv。见《从 Prompt 到 Structured Output》。
- 修复是最后一层。
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 收什么、你的字段合同是什么,不应跟着变。