JSON1

语法

JSON 转义

你从日志里复制出一段东西,完全读不了 —— 每个引号前面都有反斜杠,花括号还被包在引号里。这不是坏了。你看到的是一份被当成 JSON 字符串存起来的 JSON 文档,而回去的路是有章法的。

转义是为了什么

JSON 字符串用双引号界定。所以一旦里面的文本自己需要一个双引号,冲突就来了:解析器会把那个引号当成字符串结束。转义的办法是在前面加个反斜杠 —— \" 表示一个真正的引号字符,不是界定符。

同一招也覆盖其他不能原样出现在字符串里的东西:反斜杠本身,以及 U+0020 以下的控制字符。

JSON 定义的全部转义序列。没有别的了。
转义含义
\"一个双引号,不是字符串结束
\\一个真正的反斜杠
\n换行
\r回车
\t制表符
\b \f退格与换页
\/正斜杠。合法,但从不必需
\uXXXX按码点表示任意字符

哪些不需要转义

这点值得单独讲,因为转义过度和转义不足一样常见。非 ASCII 文本完全不需要转义。JSON 本身就是 Unicode,所以带音标的字母、汉字、emoji 原样放在字符串里都是合法的。

  • 正斜杠永远不需要。 https://example.com 直接写就行。\/ 是合法的,有些编码器会输出它,这就是日志里的 URL 常常莫名其妙很难看的原因。
  • 单引号永远不需要。 它在 JSON 里没有特殊含义,所以 \' 不是合法转义,是错误。
  • 控制字符永远需要。 字符串里出现真正的制表符或换行是非法 JSON。它们变成 \t\n;更冷僻的会变成 \u0001 这种形式。

输入 JSON

{"s":"café 中文 😀"}

转义后

"{\"s\":\"café 中文 😀\"}"
只有结构性的引号多了反斜杠。café中文 和 emoji 都原样穿过去了 —— 写成 \u00e9 也合法,只是没人读得懂。

控制字符

字符串值里的制表符和换行不能直接写,所以会变成两个字符的转义序列。这是转义唯一一处会让长度变化到肉眼可见的地方:多行字符串会明显变长。

输入 JSON

{"s":"a\tb\r\nc"}

转义后

"{\"s\":\"a\\tb\\r\\nc\"}"
输入里的 \t 本来就是个转义序列。转一轮之后变成 \\t —— 一个反斜杠加一个 t,这就是外层如何把它保住的。

反斜杠为什么会翻倍

这才是那团乱麻真正的来源。某个服务要记录请求体,做法是把整份 JSON 文档塞进另一份 JSON 文档的一个字符串字段里。内层文档的引号现在处于字符串内部,于是每一个都得到一个反斜杠。

原始数据

{"user":{"name":"Ada"},"ok":true}

作为 JSON 字符串存起来

"{\"user\":{\"name\":\"Ada\"},\"ok\":true}"
花括号现在在引号里面了:整份文档变成了一个字符串值。

两层,以及翻倍

做两次 —— 某个服务记录了一条本身就含着已记录数据的消息 —— 第一轮产生的每个反斜杠现在自己也需要转义。一个引号变成 \\\":一个被转义的反斜杠,跟一个被转义的引号。

所以数量看着像是随机的。其实不是:每一层大致让反斜杠翻一倍,所以 1 个、3 个、7 个反斜杠分别对应一层、两层、三层编码。

一层

"{\"user\":{\"name\":\"Ada\"},\"ok\":true}"

两层

"\"{\\\"user\\\":{\\\"name\\\":\\\"Ada\\\"},\\\"ok\\\":true}\""
数一下引号前面有几个反斜杠,就知道要去转义几次。三个就是两次。

把数据还原出来

去转义就是反向操作,一次一层。把转义过的字符串贴进去,就能拿回格式化好的文档:

  • 按一次去掉一层。 双重编码的字符串还原出来仍然是个转义过的字符串,不是文档。再按一次。
  • 没东西可去掉时按钮是禁用的。 只有当输入能解析成字符串、而这个字符串本身又能解析成 JSON 时它才亮起 —— 所以误点一下不会把一份正常文档搞坏。
  • 贴进转义过的数据时格式化工具会认出来。 它会明确告诉你,而不是默默重新排版,因为「恰好含着 JSON 的字符串」和「JSON 文档」是两回事。

转义过的输入

"{\"user\":{\"name\":\"Ada\"},\"ok\":true}"

去掉转义后

{
  "user": {
    "name": "Ada"
  },
  "ok": true
}
这是格式化工具上「去除转义」按钮的真实输出。

从源头避免

嵌套编码几乎都是意外。如果产生它的代码在你手上,下面这些是修法,按收益从大到小排。

  • 日志记结构,别记字符串。 多数日志库的字段可以直接收一个对象。logger.info({ body: payload }) 会正确嵌套;JSON.stringify(payload) 制造的就是你后面要一层层剥的那一层。
  • 存进 JSON 列之前不要先 stringify。 Postgres 的 jsonb 之类直接收值。先 stringify 存进去的是个字符串,之后每次查询都要加一次类型转换。
  • 永远不要用拼字符串的方式造 JSON。 转义不足就是这么来的 —— 某人姓名里的一个引号提前结束了字符串,整份数据就非法了。用库去序列化。
  • 检查消息队列里的内容。 本来就是 JSON 的消息体,进入一个承载文本的队列时不需要再编码一次。