← 全部文章

在 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 和自定义集合

MapSet 的序列化结果和大多数人的预期不一样:

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 生成类型;另外,把传输格式的类型和包含 DateBigInt 或类实例的应用模型分开。

转义与还原 JSON 字符串字面量

当你要的不是 API 请求体,而是一个转义后的字符串字面量时,用 JSON Stringify

示例输入文本:

{"message": "Hello\nworld"}

Stringify 之后:

"{\"message\": \"Hello\\nworld\"}"

适用场景:

  • 把一个看着像 JSON 的值放进另一份 JSON 文档里。
  • 在数据库的文本列里存原始 JSON 文本。
  • 把字符串安全地粘进 shell 命令或测试数据。
  • 用 Unstringify 解开双重编码的日志值。

这个工具把核心的转义/还原流程放在你的浏览器里跑。它用的就是上面代码里那套原语:转义用 JSON.stringify(text),还原用 JSON 解析。

实用检查清单

在生产代码里调用 JSON.stringify 之前:

  1. 先想清楚你要的是紧凑 JSON、美化 JSON,还是一个 JSON 字符串字面量。
  2. 绝不要把用户输入拼接进 JSON 文本。
  3. 发送请求体时带上 Content-Type: application/json
  4. 有意识地转换 BigIntMapSetDate 和自定义类。
  5. 打日志之前,用白名单或 replacer 把机密脱敏。
  6. 把循环引用当成数据结构问题看待,而不只是序列化问题。
  7. 精度要紧的大 ID,保持字符串。
  8. 只有当原始文本相等真的重要时,才用 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()) 返回 {}

MapSet 不会把自己的条目暴露成可枚举的对象属性,所以 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 工具与指南

参考资料

最后校订于 2026 年 8 月。