處理 JSON 資料時,你幾乎一定會遇到 SyntaxError: Unexpected token 這個錯誤訊息。處理 API 回應時、讀取設定檔案時,突然跳出這個錯誤,整個流程就卡住了。
想快速找到錯誤位置的話,把 JSON 貼上 JSON 格式化器 就能看到錯誤所在行號與原因。
原因只要知道,修正其實很簡單。但光看錯誤訊息,很多時候很難判斷問題出在哪裡。這篇文章說明 JSON 解析錯誤的讀法,以及 5 個最常見的原因,都附帶具體範例。
怎麼讀 SyntaxError: Unexpected token
在瀏覽器或 Node.js 中解析 JSON 時,如果語法有問題就會拋出錯誤。以下是一個壞掉的 JSON(物件的值用了單引號):
{
"name": "Alice",
"age": 30,
"city": 'Tokyo'
}
現行 V8(Chrome 與 Node.js 使用的引擎)會回報:
Unexpected token ''', ..." "city": 'Tokyo'
}" is not valid JSON
這個訊息包含兩項資訊:
Unexpected token '''— 解析器遇到了它不預期的字符'(單引號)..." "city": 'Tokyo'之後的部分 — 錯誤位置前後截取的文字片段
舊版引擎會回報 Unexpected token ' in JSON at position 14 這樣的 position 數字(從開頭數第幾個字符),但現行訊息改為引用錯誤周圍的文字。兩種寫法都在後面對照表中整理。不管是哪種格式,換行與空白都會計入,手動數字符在大型 JSON 檔案中不切實際。使用 JSON 格式化器 就能立刻定位。
5 個最常見的原因
1. 行尾多逗號(trailing comma)
JavaScript 的陣列或物件末尾加逗號也不會報錯。但 JSON 規格不允許行尾多逗號。
{
"name": "Alice",
"age": 30,
"city": "Tokyo",
}
要刪除 "Tokyo" 後面的那個逗號。
{
"name": "Alice",
"age": 30,
"city": "Tokyo"
}
"Tokyo" 後面的逗號就是原因。現行 V8 會回報:
Expected double-quoted property name in JSON at position 53 (line 5 column 1)
解析器讀完逗號後期待下一個鍵值對,但實際來的是閉括號,所以回報「期待雙引號包裹的屬性名稱」。這跟陣列的行尾多逗號行為不同。
陣列中也會出現同樣的錯誤,但回報方式不一樣:
{
"colors": ["red", "green", "blue",]
}
現行 V8 對這個案例回報:
Unexpected token ']', ..."", "blue",]
}" is not valid JSON
陣列行尾多逗號後面緊接的字符是 ],所以 V8 把它當作 unexpected token 回報。把 "blue" 後面的逗號刪掉就能解析。如果你的編輯器格式化器有自動補逗號的設定,JSON 檔案建議關掉。
2. 單引號
Python 字典或 JavaScript 物件可以用 ',但 JSON 只接受雙引號 "。
{'name': 'Alice'}
這不是合法的 JSON。全部要換成雙引號。
{"name": "Alice"}
Python 中把字典轉成 JSON 字串時,請用 json.dumps() 而不是 str()。str() 會輸出單引號。
3. 鍵沒加引號
JavaScript 中物件的鍵如果是合法識別碼就不需要引號。JSON 中,鍵必須用雙引號包起來。
{name: "Alice"}
正確寫法:
{"name": "Alice"}
4. 註解
設定檔案中很常犯的錯誤。JSON 規格沒有註解語法。
{
// 使用者名稱
"name": "Alice"
}
// 與 /* */ 都必須移除。另外 tsconfig.json 等部分檔案使用 JSONC(JSON with Comments)擴展規格,允許註解存在,但標準 JSON 解析器無法讀取。
如果你希望設定檔案保留註解,可以考慮用 YAML 管理。YAML 以 # 原生支援註解,兩種格式需要時可以互轉。
5. BOM(Byte Order Mark)
UTF-8 檔案開頭如果有 BOM(\uFEFF),解析器會把它當作非法字符。現行 V8 回報:
Unexpected token '', "{
"name"... is not valid JSON
第一個 ' 後面那個看起來什麼都沒有的字符就是 BOM。舊版引擎回報的是 Unexpected token in JSON at position 0。移除 BOM 有三種方法:
- VS Code:點狀態列的編碼顯示,選「以編碼儲存」,選不含 BOM 的「UTF-8」
- 命令列:
sed -i '1s/^\xEF\xBB\xBF//' file.json - 十六進位編輯器:刪除檔案開頭 3 個位元組(UTF-8 BOM 的
EF BB BF)
如果無法控制檔案的儲存方式(例如讀取使用者上傳的檔案),就在程式端移除開頭的 BOM:
const cleaned = text.replace(/^\uFEFF/, '');
const data = JSON.parse(cleaned);
其他原因
除上述 5 個典型原因外,以下情況也會導致解析錯誤:
括號不配對
缺少閉括號 } 或 ] 時,多數情況下錯誤在檔案末尾才回報。嵌套很深的 JSON 用肉眼找不配對的括號非常困難。使用能顯示括號配對的格式化器可以節省大量時間。
字串中的控制字符
字串內如果有未轉義的換行或 tab,就會出錯。換行要用 \n 來轉義。
{"message": "Hello
World"}
正確寫法:
{"message": "Hello\nWorld"}
開頭多零的數字
JSON 數字不能以零開頭。007 是非法值,要改成 7 這樣的標準數字寫法。
undefined 與 NaN
JavaScript 的 undefined 與 NaN 不能作為 JSON 值。含有 undefined 的物件序列化時,多數序列化器會跳過那個鍵或拋出錯誤。NaN 與 Infinity 同樣會被拒絕。
舊版與現行錯誤訊息對照表
如果你對照的是舊版指南、快取在 Stack Overflow 的回答、或這篇文章較早的版本,發現用語跟自己的主控台對不上,原因就是:JSON.parse() 的錯誤訊息隨 JavaScript 引擎演進而改變。本文其他部分主要以現行用語為主,但也保留舊用語方便搜尋。
主要變化有兩點。第一,帶引號的 token 格式(Unexpected token 'X', "..." is not valid JSON)不再回報 position 數字,改為引用錯誤周圍的文字。第二,Expected ... in JSON at position N 格式雖然保留 position,但現行多了 (line L column C),舊版沒有這個資訊。
| 本文對應位置 | 舊版訊息 | 現行 V8 輸出(實測) |
|---|---|---|
| 開頭範例(單引號) | Unexpected token ' in JSON at position 14 | Unexpected token ''', ..." "city": 'Tokyo'}" is not valid JSON |
| 原因 1:物件行尾多逗號 | 本文未引用(僅說明「閉括號成為 unexpected token」) | Expected double-quoted property name in JSON at position 53 (line 5 column 1) |
| 原因 1:陣列行尾多逗號 | 本文未引用(與物件相同說明) | Unexpected token ']', ..."", "blue",]}" is not valid JSON |
| 原因 3:鍵沒加引號 | 本文未引用 | Expected property name or '}' in JSON at position 4 (line 2 column 3) |
| 原因 4:註解 | 本文未引用 | Expected property name or '}' in JSON at position 4 (line 2 column 3) |
| 原因 5:BOM | Unexpected token in JSON at position 0 | Unexpected token '', "{ "name"... is not valid JSON |
JSON.parse(undefined) | Unexpected token u in JSON at position 0 | "undefined" is not valid JSON |
JSON.parse({ name: "Alice" }) | Unexpected token o in JSON at position 1 | "[object Object]" is not valid JSON |
| HTML 回應(下方環境對照表) | Unexpected token < in JSON at position 0 | Unexpected token '<', "<!DOCTYPE html>" is not valid JSON |
「現行 V8 輸出」欄的字串,是拿本文中使用的 JSON 範例直接丟給 JSON.parse() 實測的結果(Node v26.3.1 / V8 14.6)。執行腳本與輸出結果已提交在 scripts/benchmarks/json-parse-error-messages/,每一行都可以復現。至於哪個 V8 版本開始改變用語,尚未驗證,本表確認的只是「現行 V8 目前回傳這些訊息」這個事實。
依症狀判斷:token u / token o / end of input
錯誤訊息中出現的字符本身,就能大幅縮小原因範圍。以下三種變體都不是 JSON 檔案內容的問題,而是「交給解析器之前」就出了狀況。另外,如果 token 是 <,代表回傳的是 HTML 而非 JSON(參見下方環境對照表)。
Unexpected token u in JSON at position 0
JSON.parse 收到的是字串 "undefined"。u 是它的第一個字,舊版引擎因此在錯誤訊息中以 u 標識。這幾乎可以確定你傳進去的值在這個時點就是 undefined。
const raw = localStorage.getItem("settings"); // 鍵不存在則為 null
JSON.parse(undefined); // 拋出的錯誤: "undefined" is not valid JSON
現行 V8 不再只點名一個字符,而是引用整個轉換後的字串("undefined" is not valid JSON)。解析前先確認值是否真的存在。API 回應正文為空、儲存鍵不存在、變數名打錯字,都是典型原因。
Unexpected token o in JSON at position 1
"[object Object]" 這個字串被丟給解析器時產生的錯誤。position 0 的 [ 看起來像陣列開頭,所以舊版引擎回報在 position 1 的 o 失敗。當把 JavaScript 物件本身(而非 JSON 字串)傳進 JSON.parse,隱式的字串轉換就會產生這個形態。
const data = { name: "Alice" };
JSON.parse(data); // data 被轉為 "[object Object]"。拋出的錯誤: "[object Object]" is not valid JSON
那個值已經是解析完成的状态。直接用就好,如果目的是深拷貝,用 structuredClone(data) 而不是 stringify / parse 來回。
Unexpected end of JSON input
JSON 還沒完結字串就結束了。常見情況兩種:空字串(JSON.parse(""))與被截斷的回應(中斷的網路請求或寫了一半的檔案)。先把原始字串的長度 log 出來。長度為 0 就代表問題在解析器之前,不在 JSON 語法本身。
Big5 編碼與 JSON 解析
台灣的舊系統、傳統業軟、以及不少政府與教育機構的資料交換格式,至今仍使用 Big5 編碼。Big5 是雙位元組編碼,與 UTF-8 的位元組結構完全不同。當一個以 Big5 儲存的 JSON 檔案被當作 UTF-8 來讀取時,會產生以下問題:
SyntaxError: Unexpected token ½½ in JSON at position 0
或更常見的情況——Big5 中的漢字位元組被 UTF-8 解碼器解讀為亂碼,導致解析器在第一個字符就失敗:
SyntaxError: Unexpected token ã½¥ in JSON at position 1
這裡 ã½¥ 其實是 Big5 位元組被誤讀為 UTF-8 後的結果。{ 在 Big5 中仍是 0x7B(與 UTF-8 相同),所以通常第一個字符能讀對,但緊接著的 Big5 雙位元組內容會被拆成多個無意義的 UTF-8 字符。
排查方法:
- 用
file -bi或chardet檢查檔案的實際編碼 - 如果是 Big5,先用
iconv轉成 UTF-8 再解析:
iconv -f BIG5 -t UTF-8 config.json -o config-utf8.json
- 如果是 Big5 with BOM(某些 Windows 舊版編輯器會加上),先移除 BOM 再轉碼
Big5 與 UTF-8 的混用在以下場景特別容易發生:
- 從舊版 ERP 系統或 Oracle 資料庫(
AL32UTF8以外的字元集)匯出的 JSON - 政府機關或學校的資料交換(許多仍使用 Big5 作為交換格式)
- 用 Notepad++ 或 UltraEdit 以 Big5 編輯後,再用 VS Code(預設 UTF-8)開啟
- 跨機器傳輸時,一台使用 Windows 繁體中文(系統預設 Big5),另一台使用 UTF-8
BOM 的問題在 Big5 環境中更複雜。Big5 with BOM 的標記是 0xFE 0xFF(兩個位元組),與 UTF-8 BOM(0xEF 0xBB 0xBF,三個位元組)完全不同。如果你的程式假設 BOM 固定是 UTF-8 的三位元組來移除,遇到 Big5 BOM 時會把前兩個正常位元組當作 BOM 刪掉,導致檔案內容損毀。
// 不安全:假設 BOM 一定是 UTF-8 的 3 位元組
const cleaned = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
// 較安全:同時處理 UTF-8 BOM 與 Big5 BOM
function stripBom(buf) {
if (buf.length >= 3 && buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
return buf.slice(3); // UTF-8 BOM
}
if (buf.length >= 2 && buf[0] === 0xFE && buf[1] === 0xFF) {
return buf.slice(2); // UTF-16 BE BOM / Big5 with BOM
}
return buf;
}
如果你的 JSON 來源你控制不了(例如第三方 API、政府開放資料、或舊版系統匯出),在程式端先做編碼偵測與轉換,再交給 JSON.parse,可以避開這一整類問題。
不同環境的錯誤訊息差異
同樣的語法錯誤,在不同運行環境下錯誤訊息不同。以下是主要環境的對照,方便你搜尋時找到對應描述:
| 環境 / 運行時 | 典型錯誤訊息 | 能判斷的事 |
|---|---|---|
| Chrome / Node.js(V8) | Unexpected token '<', "<!DOCTYPE html>" is not valid JSON | 開頭的 < 代表回傳的是 HTML 錯誤頁面而非 JSON。到網路標籤檢查實際回應 |
| Firefox(SpiderMonkey) | SyntaxError: JSON.parse: unexpected character | MDN 將此格式記錄為 JSON.parse: <原因>(JSON_bad_parse在新分頁中開啟)。各引擎對應哪種文案 MDN 未明確標註 |
| Python | json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 3 column 5 | 同時顯示行號與欄位號 |
| Java(Jackson) | JsonParseException: Unexpected character ('}' (code 125)): was expecting double-quote to start field name | 同時指出預期的字符與實際字符 |
如果在瀏覽器看到 Unexpected token '<', "<!DOCTYPE html>" is not valid JSON(或舊版的 Unexpected token < in JSON at position 0),幾乎可以確定 API 回傳的是 HTML 錯誤頁面(404 或 500 頁面),不是 JSON。應該懷疑請求位址或狀態碼,而不是 JSON 語法。
Python 中常見的 Expecting value: line 1 column 1 (char 0) 也是同一類型。回應正文為空或傳了非 JSON 內容給 json.loads() 時會出現,先輸出回應內容本身是最快的排查方式。
排錯步驟
如果錯誤只有幾行,肉眼就能修。但 API 回應或大型檔案中,固定的排查順序能加快解決速度:
- 貼上 JSON 格式化器 查看錯誤行號與內容
- 看回報 position 稍微往前的位置。實際的錯誤常常在回報位置的前一行(例如第 10 行漏了逗號,到第 11 行才報錯)
- 檢查是否屬於上述 5 個原因(行尾多逗號、單引號、鍵沒加引號、註解、BOM)
- 如果是程式產生的 JSON,檢查序列化步驟。用字串拼接組 JSON 是最常見的 bug 溫床
- 如果是從 API 接收的 JSON,解析前先確認原始回應與編碼
用 fetch 接收回應時,先拿原始文字與狀態碼再解析,就能在一個地方區分 HTML 錯誤頁面、空回應、與截斷回應:
const response = await fetch("/api/data");
const raw = await response.text(); // 不用 response.json(),先拿原始文字
if (!response.ok) {
console.error(`HTTP ${response.status}:`, raw.slice(0, 200));
} else if (!raw) {
console.error("回應正文為空"); // Unexpected end of JSON input 的典型原因
} else {
try {
const data = JSON.parse(raw);
} catch (e) {
console.error("JSON 解析失敗:", e.message, raw.slice(0, 200));
}
}
用 FormatArc 確認錯誤位置
知道 5 個原因後,從幾百行的 JSON 中用肉眼找問題位置仍然很吃力。使用 JSON 格式化器 貼上就能看到錯誤行號與內容。
操作只有三步:
- 開啟 JSON 格式化器
- 把 JSON 貼在左側編輯器
- 點執行按鈕
如果是合法 JSON,右側顯示格式化後的結果。如果有語法錯誤,錯誤訊息會包含行號,修正該行後重新執行即可。
所有處理都在瀏覽器內完成,包含 API 金鑰或個人資訊的 JSON 也可以安心使用。資料不會傳送到伺服器。
預防解析錯誤
比起出錯後再修,幾個習慣可以擋掉大部分的解析錯誤:
- 產生 JSON 時用
JSON.stringify()(各語言的標準序列化器),不要用字串拼接 - 編輯器開啟儲存時 JSON 驗證(VS Code、IntelliJ、Sublime Text 都支援)
- CI 中加入驗證步驟。
python -m json.tool < config.json這樣的簡單檢查就能在部署前抓到壞檔案 - 手動編輯時用能即時驗證的工具
另外,不要把業務資料貼到不確定的線上工具。FormatArc 所有處理在瀏覽器內完成,不向外傳輸資料。
什麼時候 JSON 不是對的格式
如果經常需要寫註解、處理多行字串、管理複雜嵌套設定,JSON 的規格限制會讓你很痛苦。YAML 支援註解、自然處理多行文字、設定檔案可讀性通常更好。
但 YAML 對縮排敏感,也有隱式型別轉換等獨特的坑。兩種格式需要時可以互轉,視場合分開使用就夠了。兩者的完整差異詳見 YAML 與 JSON 的差異。
總結
JSON 解析錯誤的大多數案例都落在上述 5 個模式中。用錯誤訊息的 Unexpected token 與 position 作為線索可以縮小範圍,但檔案大時交給工具比較有效率。
遇到 Big5 編碼的 JSON 時,先確認編碼再解析,可以避開「亂碼」型解析錯誤。如果你的工作經常接觸舊系統的 Big5 資料,在程式端加上編碼偵測與轉換的步驟,會比事後修錯誤省事很多。

