先給結論: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 收什麼、你的欄位合同是什麼,不應跟著變。