Skip to content
返回博客
教程

JSON 字符串转义完全指南:字符、stringify 与陷阱

如何转义 JSON 字符串:哪些字符需要转义、escape 与 JSON.stringify 的区别、JSON 套 JSON 嵌套、Unicode 转义,以及破坏 payload 的常见陷阱。

9 分钟

JSON 字符串转义完全指南:字符、stringify 与陷阱

转义(escape)一个 JSON 字符串,意思是把任意文本变成可以安全地作为字符串字面量放进 JSON 文档里的形式。有少数几个字符(双引号、反斜杠,以及换行、制表符这类控制字符)要么带有结构含义,要么根本不允许字面出现在 JSON 字符串里,所以每一个都要替换成安全的转义序列,比如 \"\\\n。一旦做错,你的 payload 就会停止解析。

你会不断遇到这种情况:把一个 JSON 对象作为字符串字段嵌进另一个 JSON、把一段多行代码片段粘进配置值里,或者为 curl 手写一个 REST 请求体。这篇文章会讲清楚哪些字符需要转义,理清 escape 与 JSON.stringify 之间的混淆,走一遍 JSON 套 JSON 的嵌套和 Unicode 转义,并列出那些悄悄破坏 payload 的陷阱。如果你只是想立刻转义点什么,我们的 JSON 转义工具可以直接在浏览器里完成,不过它为什么这样工作也值得读下去。

什么是 JSON 字符串转义?

JSON 字符串转义,就是把原始字符串转换成可以安全嵌入 JSON 文档的形式。JSON 保留了一小组带有结构含义的字符:双引号 " 用来界定字符串,反斜杠 \ 用来开启一个转义序列。除此之外,低于 U+0020 的控制字符(换行、制表符、回车)根本不允许字面出现在 JSON 字符串里。转义会把这些字符各自替换成一个安全序列,让得到的字符串放到哪里都能干净地解析。

那什么时候真正需要它?有几种情况反复出现:

  • JSON 套 JSON:一个 webhook 信封、一条 Kafka 消息或一行审计日志会把请求体作为字符串字段存储,所以内层的 JSON 必须先转义,才能被赋值进去。
  • 手写配置:把一段多行 shell 脚本、SQL 查询或代码片段塞进单个 JSON 值,意味着要把每个换行变成 \n
  • REST 请求体:为 curl 或某个 HTTP 客户端手工拼一个 JSON 体,其中的引号和换行必须能挺过 shell 和网络传输。
  • 日志安全编码:把用户提供的内容写进结构化日志行,又不能让一个被注入的引号或换行破坏掉格式。

关于操作顺序:如果你是从凌乱或不可信的 JSON 出发,先校验它,确保你转义的是格式良好的东西。把它粘进 JSON 格式化工具美化并检查,再去转义干净的结果。转义垃圾只会得到转义后的垃圾。

哪些字符在 JSON 中必须转义

JSON 规范定义了一份精确而简短的清单。有七个字符有专属的两字符转义,其余所有低于 U+0020 的字符都退化为 \uXXXX Unicode 转义。下面是完整的 JSON 转义字符集合:

字符转义为说明
" (U+0022)\"字符串定界符
\ (U+005C)\\转义引导符(即 json escape backslash 的情况)
换行 (U+000A)\n
回车 (U+000D)\r
制表符 (U+0009)\t
退格 (U+0008)\b
换页 (U+000C)\f
其它 < U+0020 控制字符\uXXXX如 U+0000 → \u0000

哪些不需要转义同样重要。正斜杠 / 是一个完全正常的字符(转义它是可选的,只在下文讲到的一个狭窄场景里才有用)。单引号永远不需要转义,因为 JSON 不把它当定界符。而每一个处于或高于 U+0020 的可打印字符,包括所有多字节 UTF-8 字符,比如 é😀,都可以照原样使用。

举个具体的例子。左边是原始输入,右边是转义后的 JSON 字符串字面量:

Input:
She said "hello"	then left.

Escaped:
"She said \"hello\"\tthen left."

双引号变成了 \",制表符变成了 \t。现在这个字符串可以安全地放进任何 JSON 解析器、日志行或请求体了。

JSON Escape 与 JSON Stringify:区别在哪?

这一点大多数教程都跳过,也让很多人困惑。转义和 JSON.stringify 不是两个不同的操作,而是同一个操作的两个视角。

JSON.stringify(value) 把任意 JavaScript 值序列化成它的 JSON 文本表示。当这个值恰好是字符串时,序列化它就意味着用双引号把它包起来并转义内部的特殊字符。这正是 JSON 转义。所以 JSON.stringify("a\tb") 返回的是七个字符的字符串 "a\tb",连引号一起。

实际要考虑的问题是你要不要那对外层引号。这直接对应到 JSON 转义工具里的 **Wrap in double quotes(用双引号包裹)**选项:

模式输入 a"b 的输出何时使用
Wrap "a\"b"一个完整的 JSON 字符串字面量,等同于 JSON.stringify。赋给变量,或粘在冒号后面。
Wrap a\"b只有转义后的主体,没有外围引号。当你自己在 JSON 文档里手敲引号时用它。

所以如果你搜的是 “json stringify” 然后落到这里,心智模型很简单:对字符串做 stringify 就等于 wrap 开的转义。不带引号的形式则是把外层引号剥掉而已。

如何在代码中为 JSON 转义字符串

一条原则:永远不要手写一串 replace() 调用。每一种主流语言都自带一个 JSON 序列化器,能正确处理引号、反斜杠、控制字符和 Unicode。直接用它。

JavaScript

const text = 'She said "hi"\nthen left.';
const escaped = JSON.stringify(text);
console.log(escaped);
// "She said \"hi\"\nthen left."

对字符串调用 JSON.stringify 会给你完整的、带引号的字面量。只想要主体?切掉首尾两个字符:JSON.stringify(text).slice(1, -1)

Python

import json

text = 'She said "hi"\nthen left.'
print(json.dumps(text))
# "She said \"hi\"\nthen left."

print(json.dumps(text, ensure_ascii=False))
# "She said \"hi\"\nthen left."  (非 ASCII 字符保留为 UTF-8)

json.dumps 默认 ensure_ascii=True,会把每个非 ASCII 字符转义成 \uXXXX,和工具里的 ASCII-safe 模式行为一致。传 ensure_ascii=False 可以保留原始 UTF-8。

PHP

<?php
$text = "café \"quoted\"\nline";
echo json_encode($text);
// "caf\u00e9 \"quoted\"\nline"  (默认把非 ASCII 转义成 \uXXXX)

echo json_encode($text, JSON_UNESCAPED_UNICODE);
// "café \"quoted\"\nline"

json_encode 默认会同时转义非 ASCII 字符和正斜杠。加上 JSON_UNESCAPED_UNICODE 可让重音字符保持可读,加上 JSON_UNESCAPED_SLASHES 可让 / 保持原样。

Go 与 Java

在 Go 中,json.Marshal(text) 返回转义后、带引号的字节:

b, _ := json.Marshal(`a "quoted" line`)
// b == `"a \"quoted\" line"`

在 Java 中,Jackson 的 objectMapper.writeValueAsString(text)org.jsonJSONObject.quote(text) 产生同样的带引号字面量。无论哪种语言,都依赖库,它早已知道每一个你会忘掉的边界情况。

把 JSON 嵌进 JSON(JSON 套 JSON)

这是人们手工转义 JSON 最常见的原因。一个 webhook 信封、一条消息队列记录或一行审计日志,常常把整个请求体作为一个字符串字段存储。要做到这一点,内层的 JSON 必须先被转义。

看一个小对象穿过两层编码:

1. Inner object:        {"a":1}
2. Escaped as a string: "{\"a\":1}"
3. Placed in envelope:  {"payload": "{\"a\":1}"}

内层对象里的每个 " 都变成了 \",整个东西被包进一对外层引号。结果是一个合法的字符串值,可以赋给 payload

更深层嵌套的麻烦在于反斜杠会成倍增加。对一个已转义的字符串再转义,会连它的反斜杠也一起转义,所以每多一层反斜杠大约翻倍:一个原本是 \" 的内层引号,往外一层变成 \\\",再往外一层变成 \\\\\"。三层深的 JSON 套 JSON 确实难读,工具在这里能省不少事。要反向操作、把内层对象从字符串里还原出来,可以把它丢进我们的 JSON 反转义工具。

Unicode 与 \uXXXX 转义

默认情况下,JSON 乐于接受原始 UTF-8。é 还是 é 还是 ,文档也因此更可读。你不需要转义任何可打印的 Unicode 字符。

那什么时候才会用到 ASCII-safe 的 \uXXXX 输出?只有当下游系统不能放心交付 UTF-8 时:老旧的 SOAP 或 XML 网关、某些日志管道、email 头部,或者必须保持纯 ASCII 的源文件。在 ASCII-safe 模式下,每个高于 U+007F 的字符都会变成一个 \uXXXX 转义,café 会变成 caf\u00e9。它更吵,但逐字节都是 ASCII,并且在任何符合规范的解析器里都能解码回原文。

这里有一个容易忽略的细节。\uXXXX 编码的是单个 16 位 UTF-16 码元,但基本多文种平面(BMP)之外的字符(表情符号、稀有文字)需要 21 位。JSON 用一个代理对(surrogate pair)来处理它们:两个 \uXXXX 转义背靠背。一个咧嘴笑脸 😀(U+1F600)会变成 \ud83d\ude00。大多数序列化器会替你完成这件事,危险在于一个手写的转义器吐出一个孤立的、未配对的代理项。

如果代理对和码点对你还是陌生领域,UTF-8 vs UTF-16 vs Unicode 编码完全指南会拆解清楚单个字符是如何映射到字节和码元的,也补上了”为什么一个表情符号需要两个转义”背后那块上下文。

反转义:把转义后的 JSON 读回来

转义有它的逆操作。要把 "a\tb" 变回真正的双行或带制表符的文本,你需要解析它:JavaScript 里用 JSON.parse(str),Python 里用 json.loads(str)。解析器逐个走过每个转义序列,重建原始字符,包括代理对。

反转义失败时,错误几乎总是 “invalid escape sequence”(非法转义序列),常见成因有几个:

  • 一个孤立的反斜杠后面跟着 JSON 不认作转义的字符,比如 \q
  • 一个生造的转义,比如 \x41。JSON 没有 \x 十六进制转义,它只用 \u
  • 一个被截断的 \u 转义,十六进制位数不足四位,比如 \u00
  • 一个游离或不配对的双引号,破坏了字符串边界。

检查每个反斜杠是否开启了某个合法转义(\n \r \t \b \f \" \\ \/ \uXXXX),以及引号是否成对。对于从日志行中间复制出来、外层引号被丢掉的转义字符串,我们的 JSON 反转义工具无论主体带不带外围引号都能接受,并都能解码。

常见的 JSON 转义陷阱

大多数损坏的 payload 都能追溯到下面六个错误之一。

**1. 双重转义。**对已经转义过的文本再转义,会把 \n 变成 \\n、把 \" 变成 \\\",于是消费端读到的是字面的 backslash-n 而不是换行。这通常发生在上游服务已经对值做过 JSON 转义、你又转义了一次的时候。先反转义检查当前状态,然后恰好转义一次。

**2. 忘记外层引号。**Wrap 关时你只得到转义后的主体,不是完整的字符串。把 hello \"world\" 直接粘到期望 JSON 值的地方是无效的,因为缺了外围引号。要么保持 Wrap 开,要么自己敲引号。

**3. 过度转义非 ASCII。**消费端本来就能处理好 UTF-8,却打开 ASCII-safe 模式,只会让输出膨胀。café 无缘无故变成 caf\u00e9:更难读、网络上更大、零收益。除非某个特定的遗留系统要求纯 ASCII,否则就关着它。

4. 条件反射地转义正斜杠。/ 的转义只在一个地方重要:内联进 HTML &lt;script&gt; 标签里的 JSON,那里子串 &lt;/script&gt; 不管 JSON 上下文如何都会提前关闭标签。把 / 转义成 \/ 能消除这个隐患。除了这一个场景,转义斜杠纯属添乱,在 REST 体、配置文件和消息 payload 里都把它关着。

**5. 手写的 replace 链。**一条手工的 replace('"', '\\"') 流水线几乎总会漏掉点什么:某个控制字符、一个退格、一个代理对。用语言自带的序列化器,它覆盖整份规范。

**6. 转义了却从不反转义(或反转义了两次)。**一次往返必须平衡。进来时转义一次,出去时反转义一次。反转义两次,你就会毁掉数据中本就属于内容的真实反斜杠。

还有一个必须记牢的区别:JSON 转义不是 URL 编码或百分号编码。它们为不同的传输解决不同的问题,把它们混在一起(先对一个值做百分号编码再做 JSON 转义,或者反过来)会产生一团两个解析器都读不干净的乱麻。URL 编码与解码实战指南讲清楚了百分号编码什么时候才是对的工具,以及它和 JSON 做的事有何不同。

常见问题

在 JSON 中转义一个字符串是什么意思?

意思是把那些对 JSON 带有结构含义的字符(双引号、反斜杠,以及换行、制表符这类控制字符)替换成安全的转义序列,比如 \"\\\n。这样得到的结果就能作为字符串字面量嵌进 JSON 文档而不破坏解析。

JSON 中哪些字符需要转义?

双引号、反斜杠、换行、回车、制表符、退格、换页各自有专属转义,其余所有低于 U+0020 的控制字符都变成 \uXXXX。可打印字符与多字节 UTF-8 无需转义;正斜杠是可选的,只在 HTML &lt;script&gt; 标签内部才重要。

JSON escape 和 JSON.stringify 是一回事吗?

基本上是同一操作的两个视角。JSON.stringify 应用于字符串时,会用双引号包裹并转义内部的特殊字符,这就是 JSON 转义。Wrap 开等于带引号的形式(与 JSON.stringify 相同);Wrap 关只给你转义后的主体,不带外围引号。

在 JavaScript 或 Python 中如何为 JSON 转义字符串?

JavaScript 用 JSON.stringify(str);Python 用 json.dumps(str)。永远依赖内置函数而不是手写的 replace 链,内置函数正确处理 Unicode、控制字符以及所有你本来会漏掉的边界情况。

为什么我的 JSON 因为多出来的反斜杠而崩了?

通常的原因是双重转义:对已经转义过的文本再转义,于是 \n 变成 \\n,消费端读到字面的 backslash-n 而不是换行。先反转义这个值检查它的真实状态,然后恰好转义一次。

我需要在 JSON 中转义正斜杠或 Unicode 吗?

两者都不需要。/ 是普通字符,只在你把 JSON 内联进 HTML &lt;script&gt; 标签、为阻止 &lt;/script&gt; 序列提前关闭它时才需要转义。Unicode 默认保持为原始 UTF-8;仅当下游系统无法处理 UTF-8 时才用 \uXXXX

标签: JSON Encoding Data Formats JavaScript