在 JavaScript 里怎么 stringify 和 parse JSON
安全地使用 JSON.stringify 和 JSON.parse 处理请求、存储、BigInt、replacer、reviver、对象强制转换、解析源文本访问与 TypeScript 类型,既不丢值,也不轻信未经校验的数据。
先给结论: 用 JSON.stringify(value) 把 JavaScript 值变成 JSON 文本。加上第三个参数(比如 2)就能美化输出:JSON.stringify(value, null, 2)。需要脱敏字段、转换不支持的值,或者只保留部分键时,用第二个参数 replacer。别用字符串拼接来构造 JSON。
最简单的样子长这样:
const payload = {
userId: 'u_123',
active: true,
roles: ['admin', 'editor']
};
const body = JSON.stringify(payload);
// {"userId":"u_123","active":true,"roles":["admin","editor"]}
这个字符串可以直接作为 JSON 请求体、文件内容、消息队列载荷或存储值。但 JSON.stringify 不是一个"让一切都安全"的魔法按钮 —— 它有规则,而这些规则正好解释了很多 bug:属性莫名消失、本来是 NaN 的值变成 null、BigInt 序列化直接报错、Map 变成 {}、以及循环引用异常。
JSON.stringify 到底做了什么
JSON.stringify(value, replacer, space) 会遍历一个 JavaScript 值并返回 JSON 字符串。而 JSON 本身只能表示:
- 字符串
- 数字
- 布尔
- null
- 对象
- 数组
其他所有东西,都必须被转换、被省略,或者被拒绝。
例子:
JSON.stringify('hello');
// "hello"
JSON.stringify(42);
// 42
JSON.stringify(true);
// true
JSON.stringify(null);
// null
JSON.stringify({ name: 'Ada', score: 98 });
// {"name":"Ada","score":98}
顶层值是允许的 —— 一份 JSON 文档可以是对象、数组、字符串、数字、布尔或 null。
用它来构造请求体
生产环境里最常见的用法是配合 fetch:
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Ada',
email: 'ada@example.com'
})
});
有两个错误反复出现:
// 错:这个对象可能变成 "[object Object]"
body: payload
// 错:手工拼的 JSON 会在引号、换行和反斜杠上翻车
body: `{"name":"${name}"}`
JSON.stringify 会替你转义危险字符。哪怕 name 里含有引号、换行、制表符或反斜杠,输出依然是合法 JSON。
你要的是 JSON 文本,还是 JSON 字符串字面量?
大家说"stringify JSON"时,其实指的是两件相关但不同的事:
| 目的 | 输入 | 输出 | 典型用途 |
|---|---|---|---|
| 序列化一个值 | JavaScript 对象、数组、数字等 | JSON 文本 | API 请求体、文件、localStorage |
| 把文本转义成 JSON 字符串字面量 | 纯文本 | 带引号并转义的 JSON 字符串 | 嵌套 JSON、日志、数据库字段、shell 参数 |
值序列化:
JSON.stringify({ name: 'Ada' });
// {"name":"Ada"}
字符串字面量转义:
JSON.stringify('She said "hello"\nThen left.');
// "She said \"hello\"\nThen left."
本站的 JSON Stringify 工具是为第二种场景做的:它把 input.txt 里的纯文本拿过来,跑一遍 JSON.stringify(text),返回转义后的 JSON 字符串字面量。点 Unstringify 就能把 JSON 字符串字面量解析回纯文本。
调试双重编码的 JSON 时,这个区别很关键。这是 JSON 文本:
{"name":"Ada"}
这是一个"内容看起来像 JSON"的 JSON 字符串:
"{\"name\":\"Ada\"}"
第二种形式要先解析一次得到内层字符串,想拿到对象还得再解析一次。
三个参数
JSON.stringify 的签名是:
JSON.stringify(value, replacer, space)
| 参数 | 必填? | 作用 |
|---|---|---|
value |
是 | 要序列化的 JavaScript 值 |
replacer |
否 | 过滤或转换值 |
space |
否 | 增加缩进,让输出可读 |
默认输出是紧凑的,这对请求体和存储很合适。需要给人看时,再用 space。
用 space 参数美化输出
replacer 传 null,缩进传 2 表示两个空格:
const user = { name: 'Ada', age: 36, active: true };
JSON.stringify(user, null, 2);
输出:
{
"name": "Ada",
"age": 36,
"active": true
}
要四个空格就传 4,要制表符就传 '\t':
JSON.stringify(user, null, 4);
JSON.stringify(user, null, '\t');
JavaScript 把数字缩进封顶在 10 个空格,字符串缩进也会被截到 10 个字符。传 100 不会得到 100 个空格的缩进。
关于美化、压缩和校验的完整流程,见如何格式化 JSON。
用 replacer 数组过滤键
第二个参数可以是一个"要保留的属性名"数组:
const user = {
id: 'u_123',
name: 'Ada',
email: 'ada@example.com',
passwordHash: 'do-not-send',
active: true
};
JSON.stringify(user, ['id', 'name', 'active']);
// {"id":"u_123","name":"Ada","active":true}
这是白名单,不是黑名单。数组里没有的键,就不会出现在输出里。
嵌套数据要小心 —— replacer 数组是按属性名在每一层上生效的:
const order = {
id: 'ord_1',
customer: {
id: 'cus_1',
email: 'ada@example.com'
}
};
JSON.stringify(order, ['id', 'customer']);
// {"id":"ord_1","customer":{"id":"cus_1"}}
嵌套的 email 字段被省略了,因为 email 不在白名单里。
用 replacer 函数转换值
replacer 函数会先对根值执行一次,然后对每个属性执行。它接收 (key, value),返回应该被序列化的值。
脱敏机密信息:
const payload = {
user: 'ada',
password: 'secret',
token: 'abc123'
};
const safe = JSON.stringify(payload, (key, value) => {
if (key === 'password') return '[REDACTED]';
if (key === 'token') return undefined;
return value;
});
// {"user":"ada","password":"[REDACTED]"}
在 replacer 里返回 undefined 会省略对象属性;但在数组里,它会变成 null,因为 JSON 数组不能有空缺的位置。
转换 JSON 表示不了的值:
const output = JSON.stringify(
{ id: 9007199254740993n, createdAt: new Date('2026-05-24T00:00:00Z') },
(key, value) => {
if (typeof value === 'bigint') return value.toString();
return value;
}
);
// {"id":"9007199254740993","createdAt":"2026-05-24T00:00:00.000Z"}
有个不易察觉的细节:replacer 第一次是以 key === '' 对整个根值调用的。如果你在那里返回 undefined,整个 stringify 的结果就是 undefined。
toJSON 钩子在序列化之前执行
如果一个对象有 toJSON() 方法,JSON.stringify 会调用它,并序列化它的返回值。
这就是 Date 会变成 ISO 字符串的原因:
JSON.stringify({ when: new Date('2026-05-24T00:00:00Z') });
// {"when":"2026-05-24T00:00:00.000Z"}
你也可以自己定义:
class Money {
constructor(cents, currency = 'USD') {
this.cents = cents;
this.currency = currency;
}
toJSON() {
return {
amount: (this.cents / 100).toFixed(2),
currency: this.currency
};
}
}
JSON.stringify({ price: new Money(1999) });
// {"price":{"amount":"19.99","currency":"USD"}}
当某个类只有一种显而易见的 JSON 表示时,用 toJSON。当同一个值在不同场景需要不同输出时(比如对外 API 响应 vs 内部日志),用 replacer。
哪些值会被丢弃、转换或拒绝
这张表值得放在调试器旁边:
| 值 | 在对象里 | 在数组里 | 在根部 |
|---|---|---|---|
undefined |
属性被省略 | null |
undefined |
| 函数 | 属性被省略 | null |
undefined |
| Symbol 值 | 属性被省略 | null |
undefined |
| 以 Symbol 为键的属性 | 被忽略 | 不适用 | 不适用 |
NaN |
null |
null |
null |
Infinity / -Infinity |
null |
null |
null |
BigInt |
抛异常 | 抛异常 | 抛异常 |
Date |
ISO 字符串 | ISO 字符串 | ISO 字符串 |
Map |
不转换就是 {} |
不转换就是 {} |
不转换就是 {} |
Set |
不转换就是 {} |
不转换就是 {} |
不转换就是 {} |
这就是数据在 stringify 之后"消失"的原因。JSON 表示不了很多 JavaScript 值,所以序列化之前,你得先决定它们应该长什么样。
Map、Set 和自定义集合
Map 和 Set 的序列化结果和大多数人的预期不一样:
const permissions = new Map([
['read', true],
['write', false]
]);
JSON.stringify(permissions);
// {}
先转换它们:
JSON.stringify([...permissions]);
// [["read",true],["write",false]]
JSON.stringify(Object.fromEntries(permissions));
// {"read":true,"write":false}
Set 同理:
const roles = new Set(['admin', 'editor']);
JSON.stringify([...roles]);
// ["admin","editor"]
如果之后还要把类型还原回来,就让 stringify 和 JSON.parse 的 reviver 配套使用:
const json = JSON.stringify({ roles: [...roles] });
const parsed = JSON.parse(json, (key, value) => {
if (key === 'roles') return new Set(value);
return value;
});
BigInt 与大数字 ID
BigInt 会抛异常,因为 JSON 没有 BigInt 类型:
JSON.stringify({ id: 9007199254740993n });
// TypeError: Do not know how to serialize a BigInt
明确地转换它:
JSON.stringify({ id: 9007199254740993n }, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
// {"id":"9007199254740993"}
普通的大数字则是另一个问题。JavaScript 的数字是 IEEE 754 双精度浮点,大于 Number.MAX_SAFE_INTEGER 的整数,在你 stringify 之前可能就已经不精确了:
Number.MAX_SAFE_INTEGER;
// 9007199254740991
如果某个 ID 必须精确,就在 JSON 里把它存成字符串:
{ "account_id": "9223372036854775807" }
不要指望解析之后再"修"回来 —— 精度一旦丢失,原来的数字就没了。
循环引用会抛异常
JSON 是一棵树,而 JavaScript 的对象图可以有环。JSON.stringify 拒绝环:
const user = { name: 'Ada' };
user.self = user;
JSON.stringify(user);
// TypeError: Converting circular structure to JSON
打日志时,可以把重复出现的对象略过:
function omitCircularReferences() {
const seen = new WeakSet();
return (key, value) => {
if (value && typeof value === 'object') {
if (seen.has(value)) return '[Circular]';
seen.add(value);
}
return value;
};
}
JSON.stringify(user, omitCircularReferences(), 2);
而对真正的 API 载荷,更好的做法是改数据结构。一个有环的对象,通常意味着传输模型本该用 ID 或嵌套摘要,而不是把整张对象图搬过去。
别用 JSON.stringify 做深拷贝
这个老招数会丢数据:
const copy = JSON.parse(JSON.stringify(value));
它会丢掉 undefined、函数、Symbol、Map、Set、BigInt、循环引用和原型信息,Date 还会变成字符串。
运行时支持的话,内存中的克隆请用 structuredClone:
const copy = structuredClone(value);
只有当你确实想要一份"JSON 兼容的数据投影"时,才用 JSON 往返这一招。
稳定序列化与规范化序列化
JSON.stringify 大体遵循 JavaScript 的属性枚举顺序。在同一个运行时里,对普通对象来说这是稳定的,但它不是给签名或哈希用的规范化方案。
如果这段 JSON 文本会被哈希、签名、按字符串值缓存,或者在快照里比较,那就选一个确定性的方案:
- 序列化之前递归排序对象的键。
- 用一个 stable stringify 的库。
- 当签名依赖精确字节时,用 JSON 规范化方案这类正式规范。
普通的 API 请求体很少需要这些。JSON 的消费方应该解析值来比较,而不是比较原始文本。
其他语言里的 stringify
Python:
import json
json.dumps({"name": "Ada"})
json.dumps({"name": "Ada"}, indent=2)
json.dumps({"name": "Ada"}, separators=(",", ":"))
对于不支持的自定义对象,Python 的 default= 选项最接近 JavaScript 的 replacer:
import json
from decimal import Decimal
def encode(value):
if isinstance(value, Decimal):
return str(value)
raise TypeError(f"Cannot encode {type(value).__name__}")
json.dumps({"price": Decimal("19.99")}, default=encode)
Go:
package main
import "encoding/json"
type User struct {
Name string `json:"name"`
}
func main() {
b, _ := json.Marshal(User{Name: "Ada"})
_ = b
}
Ruby:
require "json"
JSON.generate({ name: "Ada" })
JSON.pretty_generate({ name: "Ada" })
把 JSON 解析回 JavaScript 值
JSON.parse(text) 做的是反方向的事:接收 JSON 文本,返回对应的 JavaScript 对象、数组、字符串、数字、布尔或 null。
const text = '{"name":"Ada","roles":["admin"]}';
const user = JSON.parse(text);
console.log(user.name); // Ada
console.log(user.roles[0]); // admin
它不会执行 JavaScript。带裸键、单引号、注释、方法或 undefined 的 JavaScript 对象字面量,要么继续作为代码存在,要么先转成严格 JSON —— 不要用 eval() 把它当数据接受。
reviver 可以在解析树从叶子往根遍历的过程中转换值:
const eventText = '{"name":"deploy","createdAt":"2026-08-16T12:00:00Z"}';
const event = JSON.parse(eventText, (key, value) => {
if (key === 'createdAt' && typeof value === 'string') {
return new Date(value);
}
return value;
});
规则要写窄,别搞成"把所有看着像日期的字符串都转掉" —— 同样的模式可能误伤某个客户标识符或自由文本。
在序列化边界上解决 [object Object]
Unexpected token o in JSON at position 1 和 "[object Object]" is not valid JSON 通常是同一个原因:一个普通对象在解析之前被强制转成了文本。
const payload = { name: 'Ada' };
String(payload); // "[object Object]"
JSON.parse(payload); // 解析器收到的是 "[object Object]"
`${payload}`; // 同样是 "[object Object]"
如果这个值本来就是对象,直接用就行。需要 JSON 文本去发请求或者存储时,就在那个边界上 stringify:
localStorage.setItem('profile', JSON.stringify(payload));
const profile = JSON.parse(localStorage.getItem('profile'));
一旦只剩下字面量字符串 [object Object],修复工具是没法把键和值重建出来的。去找更早的那次字符串拼接、模板字面量、表单编码、存储写入或请求体构造 —— 结构就是在那里被毁掉的。
用 fetch 时,把各层分清楚:Response 对象不是 JSON 文本;await response.json() 返回的已经是解析后的值;await response.text() 返回原始响应体,你可以对它调用一次 JSON.parse()。这几样里任何一个解析两次,都会制造出误导性的报错。
用解析源文本访问保住大整数
现代运行时可以把基础类型的精确 token 文本,通过 context.source 传给 JSON.parse() 的 reviver。当某个数字 token 已经大到 JavaScript Number 装不下时,这就派上用场了。
const value = JSON.parse(
'{"accountId":9223372036854775807}',
(key, parsed, context) => {
if (key === 'accountId' && context?.source) {
return BigInt(context.source);
}
return parsed;
},
);
对于不支持这个特性的老浏览器、WebView 和 Node 版本,请先做特性检测再用第三个参数。而互操作性最好的接口设计,仍然是把对精度敏感的标识符直接用字符串传。
在支持的运行时里,JSON.rawJSON() 可以把已经预先校验过的、可信的基础类型文本直接序列化,而不用经过 Number 转换。别把任意用户文本传进去:raw JSON 会影响输出的语法,所以要校验精确的 token,并为老客户端准备字符串兜底方案。
从解析结果生成 TypeScript 类型时要小心
一份样本能生成有用的接口初稿,但它描述不了自己从未展示过的情况。这份样本:
{
"id": "u_123",
"middleName": null,
"roles": ["admin"],
"projects": []
}
证明不了 middleName 是否也可能是字符串、roles 是否有固定枚举、空的 projects 数组里该放什么,也证明不了某个字段会不会干脆不出现。请拿多份样本,或者 OpenAPI / JSON Schema 约定,去复核生成的类型。
interface UserResponse {
id: string;
middleName: string | null;
roles: Array<'admin' | 'editor'>;
projects: Project[];
}
TypeScript 注解不校验运行时数据:
const user: UserResponse = await response.json(); // 只是编译期的一句承诺
在不可信边界上,请让静态类型和运行时 schema 配对:
import { z } from 'zod';
const UserResponse = z.object({
id: z.string(),
middleName: z.string().nullable(),
roles: z.array(z.enum(['admin', 'editor'])),
projects: z.array(z.object({ id: z.string() })),
});
const user = UserResponse.parse(await response.json());
type UserResponse = z.infer<typeof UserResponse>;
初稿可以用浏览器本地的 JSON 转 TypeScript 工具生成。长期维护的 API,更建议从 OpenAPI 或 JSON Schema 生成类型;另外,把传输格式的类型和包含 Date、BigInt 或类实例的应用模型分开。
转义与还原 JSON 字符串字面量
当你要的不是 API 请求体,而是一个转义后的字符串字面量时,用 JSON Stringify。
示例输入文本:
{"message": "Hello\nworld"}
Stringify 之后:
"{\"message\": \"Hello\\nworld\"}"
适用场景:
- 把一个看着像 JSON 的值放进另一份 JSON 文档里。
- 在数据库的文本列里存原始 JSON 文本。
- 把字符串安全地粘进 shell 命令或测试数据。
- 用 Unstringify 解开双重编码的日志值。
这个工具把核心的转义/还原流程放在你的浏览器里跑。它用的就是上面代码里那套原语:转义用 JSON.stringify(text),还原用 JSON 解析。
实用检查清单
在生产代码里调用 JSON.stringify 之前:
- 先想清楚你要的是紧凑 JSON、美化 JSON,还是一个 JSON 字符串字面量。
- 绝不要把用户输入拼接进 JSON 文本。
- 发送请求体时带上
Content-Type: application/json。 - 有意识地转换
BigInt、Map、Set、Date和自定义类。 - 打日志之前,用白名单或 replacer 把机密脱敏。
- 把循环引用当成数据结构问题看待,而不只是序列化问题。
- 精度要紧的大 ID,保持字符串。
- 只有当原始文本相等真的重要时,才用 stable stringify。
常见问题
怎么用 JSON.stringify 美化输出?
传第三个参数:JSON.stringify(value, null, 2) 是两个空格缩进,4 是四个空格,'\t' 是制表符。数字缩进上限是 10 个空格。
为什么 JSON.stringify 把我的一些属性丢了?
JSON 没法表示 undefined、函数和 Symbol。在对象里这些属性会被省略,在数组里会变成 null。请用 replacer,或者在序列化之前先把这些值转换掉。
值里含有 BigInt 该怎么 stringify?
JSON.stringify() 遇到 BigInt 会抛异常。用 replacer 把 bigint 转成字符串;如果接收端还需要 bigint 语义,就在那边有意识地转回去。
JSON.stringify 和 JSON.parse 有什么区别?
JSON.stringify 把 JavaScript 值变成 JSON 文本,JSON.parse 把 JSON 文本变回 JavaScript 值。它们是一对操作,但对 JSON 表示不了的值来说,这个往返是有损的。
为什么 JSON.stringify(new Map()) 返回 {}?
Map 和 Set 不会把自己的条目暴露成可枚举的对象属性,所以 JSON.stringify 看到的是个空对象。Map 用 [...map] 或 Object.fromEntries(map) 转换,Set 用 [...set]。
怎么避免循环引用报错?
打日志时,用带 WeakSet 的 replacer 标记重复出现的对象。对 API 而言,请重新设计载荷,让它携带 ID 或摘要,而不是一张有环的对象图。
用 JSON.stringify 打日志安全吗?
就转义语法而言是安全的,但就隐私而言默认并不安全。除非你先用白名单或 replacer 脱敏,否则日志里可能带上 token、密码、Cookie 或用户私密字段。
在 Python 里怎么 stringify JSON?
紧凑输出用 json.dumps(value),美化输出用 json.dumps(value, indent=2),直接写文件用 json.dump(value, file)。编码 Decimal 这类自定义对象时用 default=。
怎么把 JSON 字符串字面量还原回来?
对这个字符串字面量调用 JSON.parse。如果结果又是一个看着像 JSON 的字符串,那就先解析一次拿到字符串字面量的内容,再解析第二次得到内层对象。
为什么我会看到 [object Object] 或 unexpected token o?
一个 JavaScript 对象被强制转成了文本 [object Object],然后被当作 JSON 解析。已有对象就直接用;要发请求或写存储时,先调用 JSON.stringify()。仅凭那个被压扁的字符串,原始数据是找不回来的。
TypeScript 接口能校验 API 返回的 JSON 吗?
不能。运行时接口和类型断言都会被擦除。请用 JSON Schema、Zod 或其他运行时校验器来校验外部 JSON,再从校验后的值推断或附加静态的 TypeScript 类型。
JSON.parse 怎么保住大于 Number.MAX_SAFE_INTEGER 的整数?
首选做法是把标识符作为 JSON 字符串传输。在支持的运行时里,reviver 可以从 context.source 读到精确的基础类型 token 并转成 BigInt;请做特性检测并准备兜底方案。
相关 JSON 工具与指南
- JSON Stringify —— 把文本转义成 JSON 字符串字面量,或者还原回来。
- JSON Fix —— 解析或序列化之前先修好损坏的 JSON。
- JSON 校验器 —— 检查严格 JSON 语法及出错的行列位置。
- 如何格式化 JSON —— 用
space参数美化输出。 - JSON 解析错误 —— 排查非法输入和对象强制转换。
- 什么是 JSON? —— JSON 语法、与 JavaScript 对象的差异,以及 schema。
- JSON 转 TypeScript —— 在本地生成接口初稿。
参考资料
- MDN: JSON.stringify —— 参数、replacer、space、toJSON 及不支持的值。
- MDN: JSON.parse —— 解析与 reviver 的行为。
- MDN: structuredClone —— 不走 JSON 往返的深拷贝。
- RFC 8259 —— JSON 数据交换格式。
- RFC 8785 —— 用于确定性 JSON 字节的规范化方案。
- Python json 模块 ——
json.dumps、json.dump和default=。 - Zod 文档 —— 运行时校验与推断出的 TypeScript 类型。
最后校订于 2026 年 8 月。