← 全部文章

在 JavaScript 中如何处理损坏的 JSON

现实里的 JSON 经常是脏的:尾随逗号、单引号、Python 字面量、markdown 代码围栏。学习常见模式、如何写一个安全解析助手,以及何时该使用专门的修复库。

每一个 JavaScript 开发者都见过这一幕:JSON.parse() 抛了一个 SyntaxError,应用就崩了。那段 JSON 来自一个 API、一个配置文件,或者用户剪贴板里的一段东西 —— 反正它就是解析不了。本指南会一步步讲清楚这是为什么、坏 JSON 在实际中长什么样,以及怎么从容地处理它 —— 从一个简单的 try/catch 到自动修复。

1. JSON.parse() 为什么会失败

JSON 标准(RFC 8259)是有意严格的。JSON.parse() 完全照标准实现 —— 任何偏离,无论多小,都会以一个没有局部恢复的 SyntaxError 抛出:

try {
  const data = JSON.parse('{ name: "Ada" }'); // 没引号的 key
} catch (err) {
  console.error(err.message);
  // SyntaxError: Expected property name or '}' in JSON at position 2
}

和浏览器解析 HTML(里面有内建的错误恢复模型)不一样,JSON 解析器是一道硬关。严格解析是个特性:它逼着生产方输出干净的数据。但当你不能控制数据源时 —— LLM 输出、手工编辑的配置文件、第三方 API —— 你就需要一个策略来应对这道关「砰」的关上。

2. 坏 JSON 最常见的模式

在伸手去抓修复库之前,先认清你面对的是什么。现实里几乎所有的坏 JSON 都落在以下某一类里:

用单引号而不是双引号

// ❌ 非法 JSON
{ 'name': 'Ada Lovelace' }

// ✅ 合法
{ "name": "Ada Lovelace" }

JavaScript 对象字面量接受单引号;JSON 不接受。开发者直接把一段对象字面量复制到 JSON 环境里,就会反反复复撞到这个错。

尾随逗号

// ❌ 非法 JSON
{
  "name": "Ada",
  "active": true,   // 尾随逗号
}

// ✅ 合法
{
  "name": "Ada",
  "active": true
}

现代 JavaScript 允许数组和对象里有尾随逗号。JSON 不允许。这是手工编辑的 JSON 文件里最常见的单一错误。

没加引号的对象 key

// ❌ 非法 JSON
{ name: "Ada", score: 98 }

// ✅ 合法
{ "name": "Ada", "score": 98 }

JavaScript 对象字面量不要求 key 加引号。JSON 要求所有 key 都是双引号字符串,没有例外。

JavaScript 注释

// ❌ 非法 JSON
{
  // 这是用户记录
  "name": "Ada",
  /* 2024 年新增 */
  "active": true
}

JSON 没有注释语法。注释有时会被加到配置文件里(JSONC —— 带注释的 JSON —— 是一个流行的扩展),但 JSON.parse() 会拒绝它们。

多行字符串

// ❌ 非法 JSON
{
  "description": "line one
  line two"
}

// ✅ 合法 —— 把换行转义掉
{
  "description": "line one\nline two"
}

JSON 里的字符串值必须在同一行上。字符串内部不允许出现字面换行;改用 \n 转义序列。

NaN、Infinity 和 undefined

// ❌ 不是合法 JSON
{ "ratio": NaN, "limit": Infinity, "value": undefined }

// ✅ 合法替代
{ "ratio": null, "limit": 1e308, "value": null }

它们是合法的 JavaScript 值,但不属于 JSON 规范。JSON.stringify() 会悄悄把它们转换(NaNInfinity 变成 nullundefined 属性会整个被丢掉),这可能引发一些隐蔽的往返 bug。

Python 字面量

// ❌ Python 风格 —— 非法 JSON
{ "active": True, "deleted": False, "value": None }

// ✅ 合法 JSON
{ "active": true, "deleted": false, "value": null }

JSON 一开始就是设计来在多语言之间互通的。Python 那些首字母大写的 TrueFalseNone 是跨语言数据 bug 的常见来源。

3. 「修复」和「解析」不是同一件事

把两种根本不同的操作分清楚很重要:

  • 严格解析 —— 校验输入完全是正确的 JSON、精确报告任何错误的位置、拒绝一切偏离。当你能控制数据的生产方、想要强制一份契约时用它。
  • 修复解析 —— 用一组启发式规则试着从语法上不完美的输入里恢复出一个合法 JSON 值。当数据来自不可信或不精确的源时用它(LLM 输出、用户剪贴板、老服务)。

修复 parser 会做假设。它撞见 True 就假设你想表达 true。它撞见一个尾随逗号就把它去掉。这些假设对上面那些模式几乎总是对的 —— 但它们也可能默默掩盖一份真正畸形的文档,让「修复」改了你本来的意思。

对的工具取决于场景。在一个有 schema 校验的 API 响应流水线里,你想要严格解析,这样坏数据立刻浮出水面。在一个开发者工具里,有人手贴一段从 Slack 信息复制来的 JSON,那修复解析能省时间。

值得知道的库

两个 npm 包扛起了大部分重活,各有互补的权衡:

  • jsonrepair —— 一个宽容的修复 parser。把坏 JSON 喂给它,得到一段尽力变好的合法字符串。它能处理上面那些模式(单引号、尾随逗号、没引号的 key、注释、Python 字面量、markdown 围栏、未闭合的括号、不完整的输入)。同步、零依赖、体积小 —— 内存里有完整文档时最理想。
  • clarinet —— 一个 SAX 风格的流式 JSON parser。当 JSON 是分块到达的(流、大文件、LLM 一个 token 一个 token 给你回应)、你想 key/value 一出现就反应而不是等完整文档时用它。它对语法很严格,所以如果数据源不可靠,要搭配 jsonrepair

经验法则:手里有一小段不可信的 blob 就 修复;文档很大或者是渐进到达的就 流式处理

4. 捕获错误并给用户友好的反馈

最低限度的可用模式是一个 try/catch。永远要做这件事 —— 别让 JSON.parse() 抛出未被捕获的错误:

function safeParseJson(text) {
  try {
    return { ok: true, value: JSON.parse(text) };
  } catch (err) {
    return { ok: false, error: err.message };
  }
}

const result = safeParseJson(userInput);
if (!result.ok) {
  showError(`Could not parse JSON: ${result.error}`);
}

JSON.parse() 的错误信息因 JavaScript 引擎而异。V8(Node.js、Chrome)会带一个位置提示:

// V8 的错误信息
"Expected ',' or '}' after property value in JSON at position 42"

// Firefox SpiderMonkey
"JSON.parse: expected ',' or '}' after property value in object
at line 3 column 5 of the JSON data"

位置信息很有用但和引擎绑定。如果你需要跨环境可靠的行/列报告,自己写一个递归下降 parser 能产出结构化错误,对用户展示要友好得多。

5. 什么时候自动修复 vs. 让用户确认

不是所有修复都带同样的风险。下面是一个实用的启发式:

  • 可以自动修 —— 尾随逗号、空白规范化、引号风格转换、没引号的 key、JavaScript 注释、Python 的 boolean/null 字面量。这些都是机械变换,语义上没有歧义。
  • 考虑提醒用户 —— 结构性修复,比如自动闭合一个没闭合的对象或数组({"name": "Ada"{"name": "Ada"}),或者剥掉看起来是有意写的注释。修复大概率是对的,但用户可能想确认一下。
  • 永远标注、绝不默默修 —— 像 NaN → null 这种类型转换是会改数据值的。要给用户看清楚改了什么。

一个好的修复界面会显示一个 diff:左边原始输入,右边修复后的输出。用户在拷贝或提交结果前能确认修复是对的。

6. 在浏览器里处理 JSON 带来的隐私优势

当你在浏览器里本地处理 JSON —— 用已经在跑这页的 JavaScript 引擎 —— 数据从来没离开过用户的机器。这件事比开发者意识到的更重要:

  • API key 和凭证 —— 开发者经常贴含有秘密的 JSON payload。服务端工具会把每一个请求都记下来。
  • PII 与健康数据 —— 没有数据传输的话,GDPR 和 HIPAA 合规会容易得多。
  • 公司内部数据 —— 很多安全政策禁止把内部数据贴到第三方 web 服务里。

浏览器内处理给你的是零风险的 JSON 修复:没有服务器、没有日志、没有数据留存。计算发生在一个 <textarea> 和几 KB 的 JavaScript 里。

常见问题

我怎么在 JavaScript 里处理 JSON.parse 错误?

把调用包在 try/catch 里,返回一个结构化结果,而不是让 SyntaxError 往上传(见上面的 safeParseJson helper)。永远不要在不可信输入上不加 guard 地调 JSON.parse()

有办法自动解析坏 JSON 吗?

有 —— 一个修复 parser 用启发式规则从不完美的输入里恢复出一个合法值(去掉尾随逗号、把单引号转换成双引号、把 True 转小写)。在不可信源(LLM 输出、粘贴文本)上用;不要用在有契约校验的 API 响应里 —— 那里你希望坏数据冒出来。

我什么时候该修复 JSON、什么时候该拒绝它?

当人贴了一段大致是 JSON 的东西、或者一个 LLM 产出了它时就修复;当你能控制生产方、想要强制一份 schema 时就拒绝(严格解析)。像 NaN → null 这种类型转换永远要标注出来,绝不要默默地做。

坏 JSON 最常见的成因是什么?

把一段 JavaScript 对象字面量当成 JSON —— 单引号、没引号的 key、尾随逗号。见 JSON vs JavaScript 对象 以及在 [object Object] 以及其他错误 里的语法错误参考。

7. 立刻试一试 —— 把你的坏 JSON 贴进去

如果你手里有一段畸形 JSON 字符串、现在就想修好它,JSON Fix 能处理上面所有这些模式 —— 单引号、尾随逗号、没引号的 key、Python 字面量、注释、markdown 代码围栏,等等。一切都在你的浏览器里跑,不会发到任何服务器。