← 全部文章

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 的数据库触发器)会输出含有未转义换行或 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 会识别出控制字符违规并报出精确位置。简单情况它还能自动修字符串。