ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

JSON对象与字符串转换:从序列化原理到实战避坑指南

JSON对象与字符串转换:从序列化原理到实战避坑指南 做前端和全栈这些年每天都在跟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当成了一种数据结构。后来才想明白JSONJavaScript 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; charsetutf-8。3. 不同语言环境下的转换实战3.1 JavaScriptJSON.stringify与JSON.parse的完整姿势JS里最核心的就是全局提供的JSON对象上的两个方法。基础用法人人都知道我重点说几个高阶用法。3.1.1 stringify的对象参数只挑要的字段很多人不知道JSON.stringify可以传第二个参数用来决定序列化哪些字段或者怎么做替换const user { id: 1, name: 张三, password: 123456, token: abc, email: zhangsanexample.com }; // 方案一数组白名单 const safeUser JSON.stringify(user, [id, name, email]); console.log(safeUser); // {id:1,name:张三,email:zhangsanexample.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的第二个参数reviverreviver可以让每个键值对在返回前经过一次处理。注意它不是“过滤字段”用的而是“转换值”用的。一个典型场景是日期字符串转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 Pythonjson模块的loads与dumps细节Python里的 json.loads 对应JSON.parsejson.dumps 对应JSON.stringify。但有一些差异要注意。3.2.1 默认分隔符与紧凑输出dumps默认会加空格比如 {a: 1} 这种。生产环境为了省流量通常用 separators(,, :) 去掉空白import json data {name: 张三, age: 18} text json.dumps(data, ensure_asciiFalse, separators(,, :)) print(text) # {name:张三,age:18}ensure_asciiFalse 这个参数很关键。默认情况下dumps会把所有非ASCII字符转成 \uXXXX也就是说“张三”会变成“\u5f20\u4e09”。虽然JSON.parse也能还原但日志里可读性很差传输带宽也会增加。业务系统里建议显式设置 ensure_asciiFalse。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_hookas_user) print(type(user)) # class __main__.User这也回应了热词里那个“pandas 数据类型转换”本质上数据从JSON字符串进入DataFrame前常常要先把嵌套结构解出来、把类型转正确object_hook只是其中一环。3.3 JavaJackson与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与JavaScriptSerializerC#里老项目常用 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.DeserializeOrder(json);用System.Text.Json时要注意默认情况下它是大小写敏感的反序列化时如果JSON里的字段名是 camelCase首字母小写你C#类里是 PascalCase首字母大写直接反序列化可能失败。常见做法是var options new JsonSerializerOptions { PropertyNameCaseInsensitive true }; Order order JsonSerializer.DeserializeOrder(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后端习惯驼峰命名如 userNamePython可能用 user_name前端JS却经常用 userName。跨系统对接时这是最常见的“玄学问题”。解决方案有几种约定一套通用命名规范比如全小写下划线各端显式做字段映射Java用JsonPropertyPython的json.loads后renameJS在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传JSONconst 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要嵌入到
返回列表