ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSON对象与字符串互转实战:从parse/stringify到跨语言调试全指南

JSON对象与字符串互转实战:从parse/stringify到跨语言调试全指南 干这一行久了你会发现有一类问题特别邪门接口调试的时候后端给你返回了一段看起来没毛病的JSON你把它原样复制到本地JSON.parse直接给你一个红彤彤的报错或者你在浏览器Console里明明看到了一个结构清晰的对象右键复制出来粘到代码里结果连语法检查都过不去。这种对象和JSON字符串之间的来回折腾看起来只是两个API的事但真正上手后会发现里面全是细节。这篇东西我不讲泛泛的理论就围绕JSON对象与字符串之间的转换把我在实际项目里踩过的坑、验证过的方案、沉淀下来的速查手段一次说清楚。1. JSON是字符串对象是对象先把这个根子上的概念掰清楚1.1 JSON不是JavaScript里的对象它只是一段文本很多刚入门的朋友会把JSON和JavaScript对象划等号这个印象害死人。JSON全称是JavaScript Object Notation它确实是从JS的对象字面量演化来的但它本质上是一种跨语言的数据交换文本格式给谁看的给程序互相传数据用的。你在前端写const obj {name: Tom, age: 18}这叫对象你把它JSON.stringify(obj)得到的结果{name:Tom,age:18}这才叫JSON字符串。两者最大的区别在语法约束上。JSON格式是死板且严格的对象的键必须用双引号不能是单引号更不能是裸的标识符字符串值必须是双引号包裹不允许有尾逗号不能写注释值只能是对象、数组、字符串、数字、布尔值、null这六种类型。所以你看{name: Tom}在JS里是一个合法对象字面量但它不是合法的JSON。如果你把这段文本丢给JSON.parse()必然报错。我见过太多新人把JS对象字面量直接当JSON贴进配置文件或者传给后端然后一头雾水地来问为什么解析失败。1.2 JSON对象怎么创建怎么和字符串互相倒腾这里说的JSON对象在日常口语里通常指结构长成JSON样子的JS对象。它的创建方式没有任何特殊之处就是正常的对象字面量、new Object()、类实例都行。真正和字符串产生关系的是两个核心API// 对象 - JSON字符串 const str JSON.stringify(obj); // JSON字符串 - 对象 const obj JSON.parse(str);JSON.stringify的职责是把一个JS值序列化成一段JSON文本JSON.parse的职责是把一段JSON文本反序列化成JS值。所有关于转换的坑百分之九十都出在这两个API的边界行为上。下面我展开说的每一个细节都是围绕这两个API在不同场景下的脾气展开的。2. 从对象到字符串JSON.stringify的丢失项与救场手段2.1 被静默丢弃的undefined、Function和Symbol很多人以为JSON.stringify是原样把对象拍平成字符串但实际上它在序列化时会主动丢弃一批值。最典型的就是undefined、函数和Symbol类型的值它们在对象属性中会直接被跳过在数组里则会被转成null。const obj { name: Tom, age: undefined, sayHi: function() { console.log(hi); }, [Symbol(id)]: 123 }; JSON.stringify(obj); // {name:Tom}注意看age、sayHi、Symbol键值全部消失了。这在大多数时候是合理的因为JSON格式里没有这些类型的对应表示。但有个副作用很多人没意识到你从前端把一个对象发给后端再拿回来的时候属性已经少了。如果你做了发送前stringify接收后parse的完整闭环丢失的属性是回不来的。我遇到过最典型的场景是做表单数据持久化到localStorage表单里存了一个函数作为校验器结果写入localStorage再读出来校验器没了。这不是bug这是格式边界。在业务中能进JSON的只有可序列化的数据函数逻辑永远不应该靠JSON传递。2.2 循环引用直接抛错serialize之前先想清楚给谁看如果说上面的丢失是悄无声息的那循环引用就是直接摔桌子。const a { name: a }; const b { name: b }; a.friend b; b.friend a; JSON.stringify(a); // TypeError: Converting circular structure to JSON出现循环引用的场景非常常见树形结构里有父节点引用、图结构里有双向关联、Vue/React的响应式对象里隐藏着各种环。解决思路不是硬绕而是搞清楚你序列化这团数据要给谁看、看到什么程度。如果只是要展示部分字段可以只摘出需要的那几个属性重建对象如果确实需要序列化带环的引用结构可以自己写一个replacer用WeakSet记录已经处理过的对象遇到重复引用就输出一个标记字符串如果你要的是那种一整个对象打成串再还原成原样的效果那得引入更重的序列化方案比如flatted这类库或者用图序列化算法自己实现。这里想多嘴一句JSON.stringify还有一个第三参数space很多人在需要把JSON打到日志里或存成文件时都忘记用它。JSON.stringify(obj, null, 2)输出的带缩进格式不仅人眼可读性翻倍而且在IDE里能直接折叠排查问题效率高很多。2.3 toJSON给对象装一个自定义序列化出口有些对象直接用默认方式序列化出来的结果不是你想要的。比如Date对象默认会转成ISO字符串Map、Set默认序列化结果就是个{}BigInt直接抛错。这时候你不需要手动一个个去处理只要在对象上定义一个toJSON方法JSON.stringify在序列化该对象时就会自动调用它用它的返回值代替原始对象。class Person { constructor(name, password) { this.name name; this.password password; } toJSON() { return { name: this.name }; } } const p new Person(Tom, secret123); JSON.stringify(p); // {name:Tom}这个技巧在处理对象包里藏了不该暴露的字段时特别好用。我习惯在定义实体类时顺手写一个toJSON把敏感字段、临时字段、计算字段全部过滤掉这样后面所有序列化出口接口返回、日志打印、缓存存储都自动走最干净的那条路。再说一个replacer参数它是JSON.stringify(obj, replacer, space)的第二参数作用是在序列化过程中对每一个键值对做一次拦截改写。你可以传一个数组来指定要保留哪些键类似于白名单也可以传一个函数来动态决定。比如在测试环境打印对象时局部隐藏掉某个字段用replacer比手动深拷贝清洗要快得多。3. 从字符串到对象JSON.parse的崩溃现场与排查方法3.1 空字符串、null、数字和看起来像JSON的文本先明确一个容易混淆的点JSON.parse不是只能把JSON解析成对象它可以解析出JSON格式允许的任何值包括字符串、数字、布尔值、null甚至数组。所以JSON.parse(null); // null JSON.parse(abc); // abc JSON.parse(123); // 123 JSON.parse([1,2,3]); // [1,2,3]而JSON.parse()会直接抛出SyntaxError。空字符串不是合法的JSON文本。这个坑在接第三方接口时太常见了后端返回的responseText是空串你直接JSON.parse处理崩了。正确姿势是先判断字符串长度、先trim()或者包一层try/catch。另外要特别小心的是看起来像JSON但实际不是的字符串。比如{name: Tom}——单引号非法{name: Tom}——键没加双引号非法{name: \Tom\,}——尾逗号非法{name: Tom, /*注释*/}——注释非法。这几个变体全都是JS对象字面量的合法写法但全不是合法JSON。用JSON.parse之前先确认这段文本是哪个来源生成的。如果是后端程序用标准库序列化出来的基本没问题如果是人手工在配置文件里写的就得严格按双引号规范来。3.2 载荷不能复制对象从DevTools复制出的那些坑热搜词里有一条特别有共鸣载荷不能复制对象。这说的就是在Chrome DevTools的Network面板里你看到一个请求的Payload载荷是个对象想复制到本地调试结果怎么弄都不对。这里有个很常见的坑DevTools里展示的Payload有两种形态。一种是原始字符串进入Request Payload或者Response标签页里看到的原文这个一般是纯JSON文本能直接复制来用另一种是DevTools把JSON帮你美化成对象视图尤其是预览标签页看到的是可展开的树形结构你右键复制出来的是带格式的文本大概率带着的转义和空格甚至在某些版本里你会复制到类似{name: Tom, age: 18}这种已经丢失了键双引号的粗糙文本。我现在的做法是在Console里用fetch再拉一次接口然后copy(JSON.stringify(await resp.json(), null, 2))把标准JSON字符串复制到剪切板。或者直接右键Network里的请求选择Copy-Copy response。这样拿到的字符串100%是原始载荷不会有复制对象造成的格式损耗。还有一种隐蔽的情况复制出来的文本看着干干净净但JSON.parse仍然报错。这时候你要怀疑不可见字符。我碰到过BOM头\uFEFF藏在字符串开头也碰到过从PDF或网页复制时带进了全角引号“”。全角引号和半角引号肉眼几乎分辨不出来但解析器认死理。排查方法很简单把字符串打到Console里看一眼报错信息里的position。3.3 从报错position逆向定位坏字符JSON.parse的报错一般长这样SyntaxError: Unexpected token in JSON at position 4这个position很有用。正常情况下你把原始字符串的第4个字符抠出来就能看到问题。但大JSON里数字符位置很痛苦。我的习惯是在项目里留一个可以随时调用的调试片段function tryParse(str) { try { return JSON.parse(str); } catch (e) { const pos Number(e.message.match(/position (\d)/)?.[1] || -1); if (pos 0) { console.log(error around:, JSON.stringify(str.slice(Math.max(0, pos - 10), pos 10))); } throw e; } }这个片段能把出错位置前后的文本截出来。很多次我都靠它定位到一段被截断的JSON末尾少了}或者中间混入了一个看不到的\u0000。记住一个原则调试JSON解析问题时不要用眼睛去读整个字符串要直接看报错位置的上下文。3.4 字符串没问题但还是反序列化失败类型映射的锅在后端世界里经常出现字符串本身完全合法但转换依然失败的情况。热搜词里那条failed to deserialize the json body into the target type: input: missing field就是典型。这类问题的根源通常是JSON结构和你定义的实体类字段对不上。比如JSON里是userNameJava类字段是usernameJSON里是created_at下划线风格C#类字段是CreatedAtPascalCase前端把字段值传成了null而Java基本类型int不接受null前端用的long类型ID超过2^53在JavaScript里已经失去精度后端拿到的已经是错误数字。每种语言的解法不同但排查思路是共通的拿到原始JSON字符串对照实体类字段逐个检查命名、类型、嵌套层级。尤其要注意后端框架的命名策略比如Jackson默认映射Java字段名但配置了SNAKE_CASE后必须用JsonProperty(created_at)或全局命名策略对齐。4. 跨语言场景下的JSON互转Python、Java、C#处理上的细节差异4.1 Python里json.loads/dumps的常规操作与ensure_ascii陷阱Python标准库的json模块是日常处理JSON的主力。两个核心函数和JS里的JSON.parse、JSON.stringify一一对应import json obj json.loads({name: Tom}) # 字符串 - 字典对象 text json.dumps(obj) # 字典对象 - 字符串Python端最坑的是中文乱码问题其实不是乱码是json.dumps默认会把非ASCII字符转成\uXXXX转义序列。如果接口要返回中文给前端你不加ensure_asciiFalse就会看到一坨\u6d4b\u8bd5而不是测试。json.dumps({msg: 测试}, ensure_asciiFalse) # {msg: 测试}另外Python的json.dumps默认会把元组(1,2)序列化成数组[1,2]反序列化时数组也只能还原成列表list不会自动变回元组。如果你需要保持元组类型得自己写object_hook这是另一个话题了但很多人在这里栽过跟头。4.2 Django查询结果怎么转JSONQuerySet不是给json用的热搜词里那条django执行查询-删除对象涉及了Django的ORM操作。如果你想把Django查询的结果直接塞给json.dumps会得到一个TypeError: Object of type QuerySet is not JSON serializable。因为QuerySet是一个惰性查询集合不是纯数据。常见做法是先把它转成列表或字典data list(MyModel.objects.values(id, name, created_at)) return JsonResponse(data, safeFalse)values()返回的是QuerySet元素是字典list()之后才能被json.dumps处理。如果模型里有Decimal、datetime、UUID这些类型json.dumps还会继续报错常用的兜底方案是json.dumps(data, defaultstr)把不可序列化对象统一转成字符串。这样虽然会丢失一些类型信息但至少能顺利输出。JsonResponse内部其实也已经处理了datetime但如果你在异步任务、Celery里做序列化defaultstr是必须记得的一笔。4.3 Java/C#反序列化字段命名、缺字段和数字精度Java生态我日常用Jackson和Fastjson较多。反序列化最常见的两个错误是字段缺失和类型不匹配。missing field报错指的就是JSON里没有实体类里声明的必填字段。Jackson中如果一个字段在JSON里不存在但类里没有设默认值就可能抛MismatchedInputException。解决方式可以不全局放宽校验而是利用注解精确控制JsonIgnoreProperties(ignoreUnknown true) class UserDto { JsonProperty(user_name) private String userName; }ignoreUnknown true解决JSON里有额外字段但不认识的问题JsonProperty解决命名不一致的问题。这里建议不要一上来就把全局FAIL_ON_UNKNOWN_PROPERTIES关掉因为它能帮你挡掉很多因为接口字段改动而导致的静默bug保留对未知字段的敏感度在联调阶段是有价值的。C#这边System.Text.Json是内置方案处理大小写不一致时用PropertyNameCaseInsensitive true或者用JsonPropertyName特性var options new JsonSerializerOptions { PropertyNameCaseInsensitive true }; var user JsonSerializer.DeserializeUserDto(json, options);还有一个跨语言项目里必须警觉的问题JavaScript的number类型无法完整表示大于2^53的整数。后端生成的雪花ID、大整数主键一旦经过JS解析精度就丢了。这类字段在接口里一定要输出成字符串前端拿到字符串类型的ID才能保证还原。4.4 pandas类型转换与JSON导出数据处理场景里pandas的DataFrame和JSON打交道的机会也很多。热搜词出现pandas 数据类型转换配合JSON导出使用最常见。df.to_json(orientrecords)能把DataFrame转成一个JSON数组字符串每个元素对应一行记录。但有个高频坑当DataFrame里含numpy.int64、numpy.float64这些类型时直接json.dumps(df.to_dict())会报Object of type int64 is not JSON serializable。因为numpy的类型不是原生Python类型json标准库不认。解决的办法有几种最简单的df df.astype(object) # 转成object类型后再to_dict json.dumps(df.to_dict(orientrecords), defaultstr)如果你需要把日期列格式化输出建议先astype(str)转好再进JSON流程。这个坑在高性能计算平台导数据时非常常见处理起来不复杂但第一次遇到会愣很久。5. 高频实战转换场景数组、合并、去重与查询参数5.1 数组转字符串的四种姿势与对象数组去重数组转字符串有几种不同的语义。如果只是纯字符串数组拼成一个分隔串用join()就够了[a, b, c].join(,); // a,b,c如果是想把整个数组保留类型信息地传给后端或存储那就用JSON.stringifyJSON.stringify([a, b, c]); // [a,b,c]后端收到后再JSON.parse还原。这两种方式应用场景不同前者适合URL参数拼接后者适合结构化数据传递。对象数组去重是个经典问题。如果对象结构固定、字段顺序固定最简单的方案是拿JSON.stringify做比较keyconst arr [ { id: 1, name: a }, { id: 2, name: b }, { id: 1, name: a } ]; const unique Array.from( new Map(arr.map(item [JSON.stringify(item), item])).values() );但要注意这个方案依赖字段顺序。如果两个对象语义相同但字段顺序不同{id:1,name:a}vs{name:a,id:1}会被认为是两个不同对象。真要稳妥去重就选一个唯一标识字段做key不要用整个对象串。5.2 合并两个对象时想想是浅拷贝还是深拷贝JS合并对象最常见的写法是展开运算符和Object.assignconst merged { ...base, ...override }; // 或 const merged Object.assign({}, base, override);这俩都是浅拷贝。如果对象只有一层没问题如果嵌套了子对象后一个对象的子对象会整个替换掉前一个对象的子对象而不是递归合并。所以const base { user: { name: Tom, age: 18 } }; const override { user: { name: Jerry } }; { ...base, ...override }; // user 变成了 { name: Jerry }age 没了有人图省事用JSON.parse(JSON.stringify(obj))做深拷贝这招在简单场景确实快但它有两个硬伤第一Date对象会被转成字符串第二函数、undefined、Symbol会丢失第三循环引用直接抛错。如果你的对象要经历转成字符串这个动作这些副作用就必须正视。我自己在做配置合并、表单默认值合并时都会明确写一个递归合并函数或者引入lodash.merge而不是贪图两行代码的优雅。5.3 JS对象转QueryWrapper/URL查询参数热搜词里有个对象转querywrapper在Java的MyBatis Plus生态里确实有这种需求前端传过来一个查询条件对象后端要转成QueryWrapper。但先别急着写代码前端要做的一步通常是把对象转成URL查询串。const params { keyword: hello, page: 1, size: 20 }; const queryString Object.entries(params) .filter(([, v]) v ! undefined v ! ) .map(([k, v]) ${encodeURIComponent(k)}${encodeURIComponent(v)}) .join(); // 结果: keywordhellopage1size20这里的几个细节值得注意为什么要encodeURIComponent因为如果参数值里有中文、空格、、、#等特殊字符拼接进URL后会被歧义解析甚至触发服务端防火墙的关键字匹配。热搜词里那条waf拦截字符串mysql关键字过滤很多时候就是前端传参时没有做URL编码把SQL关键字原样拼进查询URL导致请求在网关层被拦截。把值编码成%xx形式是天然规避这类误判的有效手段。在Java后端对象转QueryWrapper可以这样QueryWrapperUser wrapper new QueryWrapper(); if (StringUtils.hasText(param.getName())) { wrapper.like(name, param.getName()); } if (param.getAge() ! null) { wrapper.eq(age, param.getAge()); }核心思路不是把对象自动转成条件而是按需eq、like、ge地拼装。把前端传来的对象直接无脑塞进QueryWrapper是很危险的做法容易产生SQL注入或全表查询风险所以这里的转换本质上是显式映射。6. 浏览器、JMeter与服务端日志三处最常用的JSON调试阵地6.1 Chrome DevTools Console里的高效转换操作我调试JSON相关问题时大部分时间都待在Chrome DevTools的Console里。有几个操作能显著提速先记一个万能函数把本地改好的对象转成JSON字符串并复制到剪切板copy(JSON.stringify(obj, null, 2));copy()是DevTools暴露的全局函数调用后可以直接粘贴到任何编辑器。这是我在Network面板里复制载荷时的替代方案不会再出现复制对象却复制出错格式的问题。另外当你从网页里拿到一段可疑的JSON文本时不要先贴进本地文件再跑程序直接在Console里执行JSON.parse(document.querySelector(script[typeapplication/ldjson]).textContent)很多页面里的结构化数据都是放在script标签里的JSON这种写法能一键把整个script文本解析成对象再慢慢看。6.2 在结果面板中查看解析后的数据另一个很有用的功能是右键对象后选择Store as global variable它会把选中的对象保存成一个全局变量比如temp1。接着你就能在Console里像操作普通变量一样操作它temp1.data.list.map(x x.name)这在处理深层嵌套JSON时太方便了不用一层一层去点开面板直接写代码筛选。比传统一层层展开对象树的效率高了一个量级。6.3 JMeter里JSON Extractor取值后到底怎么看结果jimeter 使用json extractor 取值后如何查看取到的值这个问题在接口压测/联调场景里很常见。JMeter的JSON Extractor可以把响应中某个JSONPath字段提取到变量里供后续请求使用。配置项里有几个必须对齐Variable Names你想要保存到的变量名JSONPath expressions对应的JSONPath表达式Match No.0表示随机匹配1表示第1个-1表示全部Default Values匹配不到时的兜底值建议一定填个占位符否则调试时你会分不清是没匹配到还是匹配到空字符串。配置完之后怎么确认取到了值最直接的办法是加一个Debug Sampler调试取样器放在提取器后面然后跑一次在View Results Tree里找到Debug Sampler的输出里面的vars.get(变量名)就显示实际值。变量名lemon如果你要提取的是一个数组或嵌套值注意JSONPath表达式要写对比如$.data.list[*].id。取出来之后如果要在BeanShell或JSR223脚本里用直接vars.get(变量名)就能拿到字符串。想打印到日志加一行log.info(vars.get(变量名))即可。这里我最想提醒的是提取器必须放在HTTP请求之后且作用域要覆盖后续要用这个变量的请求。放错位置会取到null这是JMeter新手的头号盲区。6.4 日志里的JSON格式化与脱敏服务端日志里打印JSON字符串时我强烈建议统一用JSON.stringify(obj, null, 2)或各语言对应的pretty print方法。一行几百KB的JSON串在日志文件里根本没法看美化成多行后定位字段时眼睛舒服很多。另外日志打JSON总容易把敏感字段带出来比如密码、token、身份证号。我现在写日志工具类时会加一个脱敏方法对常见的敏感字段做掩码处理后再打印字符串拼接和JSON.stringify的replacer都能实现成本不高但线上排障时能少卷几封安全问题邮件。7. 我把这些场景沉淀成了一张速查表说实话JSON转换本身不难难的是不同场景下该用哪种方式、要注意什么。我把这几年沉淀下来的经验整理成一张表方便你直接对照用。场景推荐手段注意事项JS对象转JSON字符串JSON.stringify(obj, null, 2)注意undefined/函数会丢失循环引用会报错浏览器复制接口载荷Consolecopy(JSON.stringify(resp))不要直接右键复制美化后的对象树JSON字符串转对象JSON.parse(str)先判空用try/catch包裹解析报错看positionPython字典转JSON字符串json.dumps(obj, ensure_asciiFalse)中文不转义datetime默认不可序列化Python JSON转字典json.loads(str)空串会抛异常数字key会变字符串Django QuerySet转JSONlist(qs.values())JsonResponseDecimal/datetime用defaultstr兜底Java反序列化缺字段JsonPropertyJsonIgnoreProperties(ignoreUnknowntrue)别全局关掉未知字段检测C#反序列化大小写JsonSerializerOptions { PropertyNameCaseInsensitive true }字段名不匹配时用[JsonPropertyName]pandas转JSONdf.to_json(orientrecords)numpy类型先astype(object)或defaultstr对象数组去重Map JSON.stringify作为key依赖字段顺序最好用唯一id做key对象转URL查询参数URLSearchParams或手动encodeURIComponent拼接过滤空值编码防止特殊字符歧义JMeter取JSON值JSON ExtractorDebug Sampler查看确认提取器作用域与Match No.配置这张表是我实际排查问题时脑子里自动过的那套映射。大多数疑难杂症最后都落到格式不是你想象的那种格式这一个根因上。最后再分享一个我个人的使用习惯只要涉及JSON转换我在代码里一定会包一层带错误上下文的封装函数把原始字符串、报错位置、出错字符这三样东西一起打出来。因为线上环境拿到的JSON来源太杂了可能是网关拼的、可能是脚本生成的、可能是人手工改的没有这层案发现场记录排查一处解析问题往往要吃两三个小时的暗亏。别嫌这个封装多花了几行代码它能帮你省下的时间远远不止这些。
返回列表