← 全部文章

JSON 字串字面量中的不合法控制字元:修復方法

JSON 字串內出現原始 tab、換行、NUL 位元組或 ANSI 跳脫序列都會觸發此錯誤。學習為何 JSON 規格禁止它們、它們如何混入,以及如何剝除或轉義。

SyntaxError: Bad control character in string literal in JSON at position N 意思是在某段 JSON 字串裡出現了一個未轉義的原始控制字元 —— 一個 tab、換行、回車,或者 U+0000–U+001F 範圍內的任何字元。JSON 規範禁止這種寫法。本文說明為什麼、它們從哪裡來,以及如何清掉。

我遇到的是哪一種字串錯誤?

什麼是控制字元?

Unicode 前 32 個碼位(U+0000 到 U+001F)是控制字元 —— 從電傳打字機時代繼承下來、不可見的格式控制碼。一些熟面孔:

碼位名稱JSON 轉義
U+0000Null
U+0008Backspace\b
U+0009水平 tab\t
U+000ALine feed(換行)\n
U+000CForm feed\f
U+000DCarriage return\r
U+001BEscape(ANSI 序列的起點)

JSON 規範(RFC 8259 §7)說得很清楚:當這個範圍裡的字元出現在 JSON 字串內部時,必須被轉義。一個原始換行夾在引號裡就是語法錯誤,不是值。

為什麼原始換行會讓 JSON 失敗

想像一個字串值裡真的有換行字元:

// 位元組層面看到的(這個換行是字面上的換行,不是 \n):
{"message":"line one
line two"}

JSON.parse('{"message":"line one\nline two"}')
// SyntaxError: Bad control character in string literal in JSON at position 20

parser 看到開頭的雙引號、繼續讀字元,然後撞上一個原始 0x0A 位元組。因為 JSON 字串必須是兩個雙引號之間、同一邏輯行上不被中斷的字元序列,這個裸換行就過早地把字串終止了。

正確的寫法是用兩個字元的轉義序列 \n

// 正確 —— \n 是轉義,不是字面換行
{"message":"line one\nline two"}

JSON.parse('{"message":"line one\nline two"}') // ✓ 可以

控制字元怎麼跑進 JSON 的

1. 用樣板字串或字串串接拼 JSON

const note = `first line
second line`;                   // 來自樣板字串裡真實的換行

const json = `{"note":"${note}"}`; // 原始換行嵌進了字串裡
JSON.parse(json);               // SyntaxError: Bad control character…

// ✓ 修法:永遠用 JSON.stringify 處理值,絕不手動內插
const json = JSON.stringify({ note });  // 自動把換行轉義掉

2. 讀檔後原樣嵌入內容

import fs from 'fs';
const content = fs.readFileSync('notes.txt', 'utf8');
// content 可能含 \n、\r\n、\t 等

// ❌ 手動拼字串 —— 嵌入了原始控制字元
const json = '{"content":"' + content + '"}';

// ✓ JSON.stringify 處理所有轉義
const json = JSON.stringify({ content });

3. 來自未轉義輸出的系統 API 回應

有些後端系統(舊 PHP 腳本、自訂 serializer、手動拼 JSON 的資料庫 trigger)會輸出包含未轉義換行或 tab 的欄位值。你收到的就是語法上不合法的 JSON,而 response.json() 會丟錯。

診斷時印出原始 body 長度與前 200 個字元。看看本該是字串值的位置裡有沒有可見的換行。

4. 從終端複製貼上(ANSI 轉義序列)

終端輸出含有 ANSI 顏色碼,它們以 Escape 字元(0x1B)開頭,後面接 [。若你把終端輸出貼進 JSON 字串,每次顏色重置(\x1B[0m)就會變成一個控制字元。

// 把終端輸出貼進 JSON 字串:
{"log":"\u001B[32mOK\u001B[0m request processed"}
//      ^^^ 原始 ESC 位元組 —— 應該是 6 個字元的 \u001B 轉義序列

5. 來自二進位資料的 null 位元組

把二進位檔案的一部分、資料庫 BLOB 欄位、或一段 C 風格字串讀進 JSON 欄位時,可能會嵌入 null 位元組(U+0000)—— 在「資料庫到 JSON」管線中這是此錯誤最常見的來源。

怎麼修

預防:永遠用 JSON.stringify

這是一勞永逸的解。JSON.stringify() 會正確地把每一個控制字元轉義 —— 你永遠不需要手動轉義。

// ✓ 不管 userInput 裡有什麼都安全
const json = JSON.stringify({ message: userInput });

事後修復:解析前剝掉或轉義

若你拿到的是外部來源的壞 JSON 而無法改生產端,可以在解析前先清洗原始字串。最安全的做法是把裸控制字元轉義掉:

function escapeControlChars(raw) {
  return raw.replace(
    /[\u0000-\u001F]/g,
    (ch) => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')
  );
}

const fixed = escapeControlChars(rawFromApi);
const data  = JSON.parse(fixed);

若這些控制字元只是雜訊(ANSI 碼、null 位元組)而非有意義的資料,直接剝掉更簡單:

// 刪掉所有控制字元,但保留 \t、\n、\r(文字裡常見)
const cleaned = raw.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, '');
const data = JSON.parse(cleaned);

另外兩種現實中的來源

  • 位置 0 的 UTF-8 BOM。 以「UTF-8 with BOM」儲存的檔案開頭是 0xEF 0xBB 0xBF。RFC 8259 禁止 JSON 含 BOM,而 JSON.parse 會以一個 position 0 錯誤拒絕,那個錯誤經常被誤讀成控制字元問題。解析前用 text.replace(/^/, '') 剝掉,或在編輯器把檔案存成「UTF-8」(不帶 BOM)。
  • VS Code:「Remove Control Characters」。 若你想手動清一個檔案,VS Code 的 Selection → Remove All Occurrences of Find Match 配合正則搜尋 [\x00-\x08\x0b\x0c\x0e-\x1f](保留真實的 \t\n\r)能一次刪掉不可見的控制位元組。若你常做這事,部分擴充功能也有 「Remove Control Characters」指令。

找出問題位置

錯誤訊息含一個位元組位置。用它去看那個地方到底是什麼:

try {
  JSON.parse(raw);
} catch (e) {
  const pos = Number(e.message.match(/position (\d+)/)?.[1]);
  if (!isNaN(pos)) {
    const context = raw.slice(Math.max(0, pos - 20), pos + 20);
    console.log('Context around error:', JSON.stringify(context));
    // JSON.stringify 會把控制字元重新轉義出來,讓你看到它們
    // 例如 "...line one\nline two..." —— 那 \n 原本是真換行
  }
}

常見問題

JSON 裡的「bad control character」是什麼?

一個夾在 JSON 字串裡、未轉義的原始字元,範圍 U+0000–U+001F —— tab、換行、回車、null 位元組或 ANSI escape。RFC 8259 §7 要求每一個這類字元都必須被轉義(例如換行寫成 \n)。

怎麼修 bad control character 錯誤?

一勞永逸的解是用 JSON.stringify() 來建 JSON,它會自動把控制字元轉義。如果你拿到的壞 JSON 沒辦法在源頭修,那就在解析前用正則把控制字元轉義或剝掉(見上方程式片段)。

為什麼原始換行會弄壞 JSON?

JSON 字串必須是兩個雙引號之間一段不被打斷的字元序列。字面意義上的換行位元組(0x0A)會提早結束字串,所以 parser 回報控制字元錯誤。合法寫法是兩個字元的轉義 \n

怎麼從 JSON 中去除 ANSI 顏色碼?

ANSI 序列以 Escape 位元組(U+001B)開頭。解析前用 raw.replace(/\[[0-9;]*m/g, '') 把它們剝掉,或一開始就別把原始終端輸出貼進 JSON 字串值。

在瀏覽器中修復壞 JSON

把你的壞 JSON 貼到 fixjson.org 的 JSON Fix。寬容的 parser 會辨識控制字元違規並回報精確位置。對簡單情況它能自動修復字串。