做前端和全栈这些年,每天都在跟JSON打交道。要说哪个操作最基础、最常见,却又最容易被忽视,我第一个想到的就是JSON对象与字符串之间的转换。别小看这一来一回的序列化与反序列化,接口联调、本地存储、跨端通信、日志上报,几乎所有环节都踩在它的肩膀上。这篇文章不打算给你念官方文档,而是把我这些年调JSON、踩JSON坑的经验一次性倒出来,希望能帮刚开始接触数据处理的同学少走弯路,也跟老手一起回炉复习一遍。
1. 为什么JSON对象与字符串的转换是绕不开的基础功
1.1 一个典型的数据流转场景
想想你这个星期写的代码:前端往接口发请求,body里装的是 JSON.stringify 后的字符串;后端拿到后先反序列化成对象,再塞进数据库;DB返回结果后,后端又序列化成JSON字符串;前端收到后 JSON.parse 还原成对象,渲染到页面上。这一圈下来,JSON对象与字符串的转换至少发生了四次。
你可能觉得这是废话,但很多人恰恰在“为什么需要转换”这个问题上栽了跟头。HTTP协议传输的是文本(字节流),内存里存的是对象(结构化数据)。对象没法直接被扔到网络上,必须先把对象的“形状”和“值”按统一的文本格式描述出来,传输的另外一端再按同样的规则重建对象。这个过程就是序列化和反序列化。
打个比方:对象像是货架上有标签、有分区、有引用的实物,字符串像一份写清楚每个格子放什么东西的清单。寄快递时不可能把货架搬过去,只能把清单发过去,收件人按清单重新把货架搭起来。JSON对象与字符串之间转换,本质上就是在“实物”和“清单”之间做双向翻译。
1.2 什么时候必须转换,什么时候不用
很多新手会问:为什么console.log打印出来明明是对象,赋值给别人却变成了[object Object]?其实就是因为在这个场景下,对象被隐式转换成了字符串,但没人用JSON.parse把它读回来。
必须转换的场景很清晰:
- 网络传输:Ajax/fetch请求体、响应体都需要文本格式。
- 本地存储:localStorage、sessionStorage只能存字符串。
- 跨语言通信:Python后端、Java服务、Go微服务之间传递数据,统一用JSON字符串作为“通用语言”。
- 日志输出:把对象完整结构打到日志里,方便排查。
不需要主动转换的场景也有:内存中直接传递对象引用、同进程内的函数传参、框架内部状态管理(Vue的reactive、React的state)等。在这些场景里强行stringify反而会带来性能损耗和引用丢失的问题。
2. 先看本质:JSON、对象、字符串三者到底有什么区别
2.1 JSON是“格式约定”,不是具体的数据结构
我刚学这个的时候,最大的困惑是把JSON当成了一种数据结构。后来才想明白,JSON(JavaScript Object Notation)是一套语法规则,是“文本的某种特定写法”。它的全称里就写了Object Notation,主要在说明对象的书写规范:键值对、花括号、中括号、逗号、双引号。
关键点来了:JSON格式里,键名必须用双引号包裹,字符串值也必须用双引号,不能有单引号,不能有尾逗号。而JavaScript对象字面量则宽松得多,键名可以不加引号,值可以用单引号。这也是我经常提醒同事的一句话:“JSON.parse的作用对象是JSON文本,不是JavaScript对象字面量。”
我记得有个经典例子:
// 这是合法的JS对象字面量 const obj = { name: '张三', age: 18 }; // 但下面这个字符串并不是合法的JSON文本 const str = "{ name: '张三', age: 18 }"; // JSON.parse(str) 会直接报错很多人写JSON字符串时习惯性照搬JS对象写法,结果一去parse就抛 SyntaxError。理解JSON是“格式约定”,能帮你少踩很多语法坑。
2.2 对象是“内存里的实体”,字符串是“文本序列”
对象是运行时的实体,它有自己的原型链、方法、非枚举属性、循环引用等。字符串只是一串有序字符。JSON.stringify做的不是“把对象变成字符串”那么轻巧,它实际做的是:
- 遍历对象可枚举的自身属性;
- 将值按JSON规则序列化;
- 函数、undefined、Symbol等值会被忽略;
- NaN、Infinity等非有限数字会被转为null;
- 循环引用会抛出 TypeError。
而JSON.parse做的是逆向:按语法规则解析文本,重建出全新的、普通对象/数组/原始值。重建出来的对象和原来那个对象已经是两个不同的实体,只是结构和值相同。这就是为什么你深拷贝时用 JSON.parse(JSON.stringify(obj)) 会丢失很多信息,原因就在这里:它只保留JSON“支持”的那部分信息。
2.3 字符串长度、字符编码对转换结果的影响
有一类热词我特别有感触:“给出一个长度为n的字符串s,其中只包含'r','g','b'三种字符”、“字符串长度”、“字符串字母大小写转换”等等。这说明很多人其实卡在“字符串”本身的处理上,而不是JSON语义。JSON字符串也是有长度和编码的,序列化之后中文可能变成 \uXXXX,也可能按UTF-8传输。如果对端解析时字符集不一致,就会出现乱码。
我在做接口联调时遇到过:后端返回的中文变成了 \u597d\u7684 这种格式,前端JSON.parse后自动还原成了“好的”,没有问题;但如果我用一个老旧的HTTP库去按ISO-8859-1解码,看到的就是一串乱码。所以关于编码,建议统一用UTF-8,并且显式地在请求头里声明:Content-Type: application/json; charset=utf-8。
3. 不同语言环境下的转换实战
3.1 JavaScript:JSON.stringify与JSON.parse的完整姿势
JS里最核心的就是全局提供的JSON对象上的两个方法。基础用法人人都知道,我重点说几个高阶用法。
3.1.1 stringify的对象参数:只挑要的字段
很多人不知道JSON.stringify可以传第二个参数,用来决定序列化哪些字段或者怎么做替换:
const user = { id: 1, name: '张三', password: '123456', token: 'abc', email: 'zhangsan@example.com' }; // 方案一:数组白名单 const safeUser = JSON.stringify(user, ['id', 'name', 'email']); console.log(safeUser); // {"id":1,"name":"张三","email":"zhangsan@example.com"} // 方案二:函数替换器 const safeUser2 = JSON.stringify(user, (key, value) => { if (key === 'password' || key === 'token') { return undefined; // 返回undefined,字段会被忽略 } return value; });这在给后端传数据、脱敏用户信息时特别有用,不用先手动解构再stringify。
3.1.2 stringify的第三参数:格式化输出
第三参数可以传数字或字符串,用来控制缩进。调试时写成 JSON.stringify(obj, null, 2),打印出来就是人类友好的排版;传文件时一般就不需要格式化了。
3.1.3 parse的第二个参数:reviver
reviver可以让每个键值对在返回前经过一次处理。注意它不是“过滤字段”用的,而是“转换值”用的。一个典型场景是日期字符串转Date对象:
const data = '{"name": "会议", "time": "2025-06-01T10:00:00Z"}'; const parsed = JSON.parse(data, (key, value) => { if (key === 'time') return new Date(value); return value; }); console.log(parsed.time instanceof Date); // true3.1.4 toJSON方法:自定义序列化行为
如果一个对象实现了toJSON方法,stringify时会优先调用它,用返回值替代对象本身参与序列化。
class Money { constructor(amount) { this.amount = amount; } toJSON() { return { value: this.amount, currency: 'CNY' }; } } const order = { id: 1001, total: new Money(199.9) }; console.log(JSON.stringify(order)); // {"id":1001,"total":{"value":199.9,"currency":"CNY"}}这个特性在领域模型设计里非常实用,可以控制敏感字段的暴露方式,也可以统一数值格式化。
3.2 Python:json模块的loads与dumps细节
Python里的 json.loads 对应JSON.parse,json.dumps 对应JSON.stringify。但有一些差异要注意。
3.2.1 默认分隔符与紧凑输出
dumps默认会加空格,比如 {"a": 1} 这种。生产环境为了省流量,通常用 separators=(',', ':') 去掉空白:
import json data = {"name": "张三", "age": 18} text = json.dumps(data, ensure_ascii=False, separators=(',', ':')) print(text) # {"name":"张三","age":18}ensure_ascii=False 这个参数很关键。默认情况下,dumps会把所有非ASCII字符转成 \uXXXX,也就是说“张三”会变成“\u5f20\u4e09”。虽然JSON.parse也能还原,但日志里可读性很差,传输带宽也会增加。业务系统里建议显式设置 ensure_ascii=False。
3.2.2 loads的object_hook与自定义类型
有时候我们希望把JSON对象自动映射成Python对象,而不是dict。用object_hook可以做到:
class User: def __init__(self, name, age): self.name = name self.age = age def as_user(d): return User(d['name'], d['age']) text = '{"name": "李四", "age": 20}' user = json.loads(text, object_hook=as_user) print(type(user)) # <class '__main__.User'>这也回应了热词里那个“pandas 数据类型转换”,本质上数据从JSON字符串进入DataFrame前,常常要先把嵌套结构解出来、把类型转正确,object_hook只是其中一环。
3.3 Java:Jackson与Gson的取舍
Java不像JS和Python那样有语言级别的JSON API,一般用Jackson或Gson。我最早用的Gson,后来切到Jackson,原因是Jackson性能更好、注解更丰富,而且Spring Boot内置了Jackson,大多数场景下直接用ObjectMapper就行。
3.3.1 ObjectMapper基本用法
import com.fasterxml.jackson.databind.ObjectMapper; ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(user); // 对象转字符串 User user = mapper.readValue(json, User.class); // 字符串转对象3.3.2 忽略未知字段
后端接口升级后,前端多传了一个字段,Java反序列化默认会抛出 UnrecognizedPropertyException。两个常见解法:
// 配置ObjectMapper忽略未知字段 mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 或者在类上加注解 // @JsonIgnoreProperties(ignoreUnknown = true)这个配置我每次搭建新项目都会加上,否则联调期间很容易被无关字段打断。
3.3.3 LocalDateTime的序列化坑
Java 8引入的LocalDateTime在Jackson 2.x早期版本里并不能直接序列化,会报 JavaTimeModule 相关的错误。解决方案是注册JavaTimeModule,并配置日期格式:
mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);这个坑特别经典。我见过很多同事把LocalDateTime字段暴露到接口后,前端收到的是数组 [2025, 6, 1, 10, 0] 而不是字符串,一脸懵。根源就是没禁用 WRITE_DATES_AS_TIMESTAMPS,或者前端没做适配。
3.4 C#:System.Text.Json与JavaScriptSerializer
C#里老项目常用 JavaScriptSerializer 或 Newtonsoft.Json,.NET Core 3.0以后官方主推 System.Text.Json,性能提升明显,API风格也更现代。
using System.Text.Json; // 对象转JSON字符串 string json = JsonSerializer.Serialize(order); // JSON字符串转对象 Order order = JsonSerializer.Deserialize<Order>(json);用System.Text.Json时要注意:默认情况下它是大小写敏感的,反序列化时如果JSON里的字段名是 camelCase(首字母小写),你C#类里是 PascalCase(首字母大写),直接反序列化可能失败。常见做法是:
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; Order order = JsonSerializer.Deserialize<Order>(json, options);有人可能想问,Newtonsoft.Json还有必要学吗?我建议:老项目维护需要会读,新项目尽量用System.Text.Json,少一个依赖,性能更好,API也更简洁。
4. 排查了几千个JSON问题后,我整理出的高频坑位
4.1 坑位一:undefined、函数、NaN在序列化时的消失与变形
前端经常有这种代码:
const payload = { name: '张三', age: undefined, score: NaN, callback: () => {} }; console.log(JSON.stringify(payload)); // {"name":"张三","score":null}你会发现age直接消失了,callback也没了,NaN变成了null。如果后端的校验要求age必须存在,这里就会出问题。处理方式是在stringify之前显式给默认值,或者用replacer统一处理。
4.2 坑位二:循环引用导致的TypeError
两个对象互相引用时,直接JSON.stringify会抛 TypeError: Converting circular structure to JSON。排查方法是用前端调试工具打断点,查看对象引用;或者先用第三方库(如flatted)序列化,再做检查。生产环境出现循环引用,通常说明状态管理里的数据模型设计出了问题,比如父组件保存了子组件实例,子组件又存了一份父组件的引用。
4.3 坑位三:JSON.parse的隐性try-catch缺失
JSON.parse失败必然抛异常,但不一定让你的程序崩溃。问题出在很多人没做容错:
let data; try { data = JSON.parse(localStorage.getItem('user') || '{}'); } catch (e) { data = {}; }localStorage里如果存了被截断的JSON字符串,parse就会抛错。我在处理本地缓存兼容性时,几乎每次都会加try-catch。尤其是在老版本App升级之后,旧的缓存字段和新代码不兼容,try-catch + 默认值是非常必要的兜底。
4.4 坑位四:数组序列化后变成对象
很多人用Array去重或者做Map结构,然后stringify再parse回来,发现类型变了。典型例子:
const map = new Map([['a', 1]]); console.log(JSON.stringify(map)); // {}Map直接序列化会变成空对象。需要用Array.from包裹成数组。Set也是同样道理。反过来,JSON.parse也不可能还原出Map和Set,因为JSON格式里没有这两种类型的概念。
4.5 坑位五:前后端字段名大小写不一致
Java后端习惯驼峰命名如 userName,Python可能用 user_name,前端JS却经常用 userName。跨系统对接时这是最常见的“玄学问题”。解决方案有几种:
- 约定一套通用命名规范,比如全小写下划线;
- 各端显式做字段映射,Java用@JsonProperty,Python的json.loads后rename,JS在stringify前transform;
- 用DTO层单独定义传输字段名。
4.6 坑位六:日志里看到 [object Object] 或 { 不可见 }
前端打日志时经常有人写:
console.log('接口返回: ' + result); // 输出: 接口返回: [object Object]原因很简单:加号触发了隐式字符串转换。正确做法是:
console.log('接口返回: ', result); // 或者 console.log('接口返回: ' + JSON.stringify(result));后者在排查问题时有一个额外的好处:可以复制出去放到Postman、Chrome DevTools或其他工具里再分析。热词里有一条“载荷不能复制对象”,应该指的就是浏览器里页面提示不能把一个对象数据当作文本复制,实际解决思路也类似:先JSON.stringify转成字符串,再复制。
4.7 坑位七:JSON格式与HTML、SQL、URL混在一起时的转义地狱
有时候你要把JSON字符串塞到HTML属性里、URL query参数里、SQL字段里。这时候嵌套转义是个大坑。比如URL传JSON:
const json = JSON.stringify({ name: '张三' }); const url = `https://api.example.com/search?data=${encodeURIComponent(json)}`; console.log(url); // https://api.example.com/search?data=%7B%22name%22%3A%22%E5%BC%A0%E4%B8%89%22%7D注意字符串里的双引号必须做URL编码,否则会被URL语法解析器切开。同理,如果JSON要嵌入到