1. 从一段“看得懂却写不对”的JSON说起
我最早接触JSON,是某次给别人做接口联调。对方丢过来一串配置,让我照着写一份类似的数据结构。当时我心想,这不就是“键值对”吗,看着毫无难度。结果真动手一写,接连闹了几个笑话:多了一个逗号导致解析直接报错,字符串该加分号的规规矩矩加了,不该加的却出现在数字旁边,更别说后续嵌套结构里引号到底该单还是双的问题,糊里糊涂调了半天。
JSON全称是JavaScript Object Notation,但它早就不是JavaScript的专属品。凡是做接口对接、写配置文件、存日志、做前后端数据传输的,几乎每天都在和它打交道。它的出现只有一件事:把“人写着舒服”和“机器读着不费劲”这两个目标统一起来。你不需要懂编译原理,也不需要背一堆API,只要掌握它那套极简的规则,就能在很多场景里直接上手工作。
这篇文章适合这样几类人:刚开始接触接口开发但只会从页面复制数据的初学者;写后端服务但一直凭感觉拼JSON字符串的开发者;还有那些被XML折磨过、想找一种更轻量的数据格式来替代的家伙。我会把JSON的写法规则、数据类型边界、嵌套设计、解析与序列化机制以及最容易踩坑的细节一一拆开讲,最后附上我实际项目里常用的排查套路。读完你会发现,JSON不难,难的是在“看着都对”的背后,知道为什么对、在什么情况下会错。
2. 先建立整体认知:JSON的核心结构
2.1 两种容器:对象与数组
JSON只提供两种容器结构,所有复杂数据都是这两种结构的自由组合。
对象用花括号{}包裹,内部由“键值对”组成,键和值之间用冒号分隔,多个键值对之间用逗号分隔。比如:
{ "name": "张三", "city": "上海", "age": 28 }这就是一整个JSON对象。注意,键必须是字符串,也就是必须加双引号,值则可以是任意合法类型。
数组用方括号[]包裹,内部是一组有序的值,用逗号分隔。比如:
["Java", "Python", "Go"]数组不关心里面装的是什么,你可以装字符串、数字、对象,甚至再装一个数组,也就是多维嵌套。
这两种容器最大的区别在于:对象描述的是“这是什么”的键值关系,数组描述的是“排在第几个”的顺序关系。大多数业务需求里,这两个结构会相互嵌套,比如说一个班级信息,外层是对象,里面的学生列表是数组,数组里每个学生又是一个对象,对象里可能再嵌套一个成绩数组。这种组合方式构成了JSON表达力的全部来源。
2.2 六种数据类型,不多不少
JSON只定义了六种数据类型:
- 字符串(string):双引号包裹的Unicode字符序列
- 数字(number):整数或浮点数,支持科学计数法
- 布尔值(boolean):
true或false - 空值(null):表示“没有值”或“值为空”
- 对象(object):无序的键值对集合
- 数组(array):有序的值列表
很多人会问,日期呢?二进制呢?不支持的。JSON里没有专门的日期类型,日期一般用字符串表示,比如"2024-01-01T10:30:00Z"这种ISO 8601格式,或用一个数字表示时间戳。二进制则通常转为Base64编码的字符串。这是JSON设计上的一种克制——它的定位就是数据交换,而不是一个完整的类型系统。
理解这六种类型非常重要。因为在设计接口返回结构和写配置文件时,常犯的错误就是把JSON当成编程语言来用,试图塞入函数、注释、NaN这类非JSON类型。这种文件一旦交给标准解析器,直接报错。
2.3 键值对设计:键的唯一性与命名习惯
JSON对象的键在同一个对象内不允许重复。虽然某些解析器在遇到重复键时只取最后一个值(有的甚至直接报错),但标准规范里规定,键名应该是唯一的,这属于JSON数据的一个基础约束。
在实际设计键名时,我会遵循一个习惯:优先使用小驼峰或下划线风格,在一个项目里保持一致。团队的接口文档如果坚持某个规范,就按规范来,但千万别出现同一个接口里一会儿userName一会儿user_name的情况。虽然JSON本身不限制命名风格,但混乱的命名会让协作成本陡增。
提示:JSON的键必须使用双引号,单引号在大多数解析器里会直接导致语法错误。
3. 语法细节拆解:为什么你的JSON老报错
3.1 字符串与转义字符的完整规则
字符串是最常用但也最容易出错的数据类型。一个字符串必须由双引号包裹,内部字符如果是普通文本,直接写就行。但当字符串里需要出现双引号、反斜杠或者控制字符时,就必须使用转义。
常见转义字符如下:
| 转义序列 | 含义 |
|---|---|
\" | 双引号 |
\\ | 反斜杠 |
\/ | 正斜杠(可选,一般不需要) |
\b | 退格符 |
\f | 换页符 |
\n | 换行符 |
\r | 回车符 |
\t | 制表符 |
\uXXXX | Unicode字符,XXXX是四位十六进制 |
举个实际例子,如果我想要JSON里保存这样一句话:
他说:"你好",路径是 C:\new\test直接写字符串是不行的,因为双引号会提前终止字符串,反斜杠也会被误解为转义起始符。正确的JSON写法是:
{ "message": "他说:\"你好\",路径是 C:\\new\\test" }这里双引号被转义为\",反斜杠被写成\\,这样解析器才能还原出原始含义。许多新手在构造这种带路径、带引号的JSON时,习惯用字符串拼接的方式手动拼,拼完肉眼一看没问题,程序一解析就炸。往后凡是遇到需要手写JSON的场景,先想清楚哪些字符需要转义,再动手。
3.2 数字类型的边界与陷阱
JSON的数字类型比编程语言里更宽松一些。它支持负数、小数点、指数表示法,比如-12.5、0.88、1.5e10都合法。但它和JavaScript一样,不区分整数和浮点数,底层统一按双精度浮点处理。
两个特别容易踩的坑:
第一,前导零非法。01、02.5这类写法在JSON里直接报错,正确的是写成1、2.5。看到从某个工具里复制出来的JSON数字带着前导零,基本可以判断这个数据源不干净。
第二,超过安全整数范围的数字会丢精度。JSON标准本身允许任意精度,但大多数编程语言的数字类型是双精度,能安全表示的最大整数在2的53次方左右。如果你传一个Snowflake算法生成的ID,比如7823948273493827394,前端解析后很可能变成另一个数。解决方式有两种:一是后端把它转成字符串返回;二是前端使用支持大数的解析库。我的建议是,凡是标识类ID字段,接口返回时统一用字符串,避免精度丢失。
3.3 布尔值与null:最容易复制错的写法
布尔值必须是true或false,全小写,不能加引号。"true"会被当成字符串,处理方法完全不同。null同理,别写成NULL或Null,那些都是程序语言里的写法,JSON只认小写。
这些坑通常出现在手工编辑配置文件时。很多配置文件是JSON格式,比如某些工具的settings.json,用户在界面或者文档里复制了一段配置,把true写成了True,运行时直接被判定为非法JSON。遇到这种报错,先检查布尔值和null的写法,十有八九能快速定位问题。
3.4 嵌套结构的缩进与可读性设计
JSON本身不在乎缩进和换行,{"name":"张三","age":28}和换行展开的写法解析结果完全一样。但人需要可读性,所以实际存储和展示时,我们几乎总是使用格式化后的多行版本。
缩进推荐使用两个空格或四个空格,在一个团队里保持统一即可。不要用Tab和空格混排,那会在diff时产生大量无关变化,掩盖真实的修改内容。
嵌套层级也不要过度设计。我见过某些项目把JSON嵌套到六层、七层,数据读起来非常痛苦。合理做法是:能平铺就平铺,对象套数组套对象这种结构控制在三层左右最好,超过四层就要考虑是否需要简化,因为每一层嵌套都会给解析、校验和排查增加负担。
4. 应用场景拆解:JSON凭什么成为主流数据交换格式
4.1 前后端接口数据传输中的角色
说到应用场景,排第一的绝对是前后端接口数据交换。前端发Ajax请求,后端返回JSON数据,这已经成为今天Web开发的默认姿势。
从后端返回JSON时,有几个细节值得注意。
响应体通常包含状态码、消息和数据三部分。比如一个标准响应结构:
{ "code": 200, "message": "操作成功", "data": { "id": "1001", "name": "测试账号" } }code用来标识业务状态,message给出人可读的提示,data才是真正业务数据的载体。设计成这种包裹结构,是为了让前端拿到响应时,先看状态码判断是否成功,再看数据。如果后端直接返回裸数据,成功与失败的判断就会变得混乱。
接口设计里还有一个常见的“列表+总数”返回结构:
{ "code": 200, "data": { "list": [ {"id": "1", "name": "项目A"}, {"id": "2", "name": "项目B"} ], "total": 2 } }这种结构的好处是前端可以一次性拿到列表数据和总数,用于分页展示,不需要额外发一个统计请求。
前后端联调时,我们常遇到的典型问题是字段类型对不上。比如后端用数字表示状态0/1,前端却期望布尔值true/false;或者后端返回的时间是时间戳,前端要的是格式化字符串。这类问题虽然不改变JSON结构,但会让联调浪费大量时间。在开发初期就约定好字段名、类型和备用值,是减少无效沟通的关键。
4.2 配置文件场景:JSON是否合适
配置文件是JSON的另一个重要应用领域。很多开发工具和框架都使用JSON格式做配置文件,比如代码编辑器的设置、构建工具的配置、静态站点生成器的站点配置等。
用JSON写配置的好处在于:结构简单,绝大多数开发者都认得;解析库遍地都是,任何语言都能轻松处理。
但JSON做配置文件有一个先天不足:不支持注释。在一个长期维护的配置文件里,无法通过注释解释某个选项的用途,会导致后来者只能靠上下文猜。拥有“聪明注释”的JSON5、支持注释的HOCON、以缩进为基础的YAML,都是为了弥补这一缺陷出现的替代方案。
那我的态度是:如果配置项很少(不超过几十个),且定义非常清晰,JSON完全可以胜任;如果配置文件可能变得很长、需要大量注释说明,我会优先考虑其他格式。不过话又说回来,JSON对工具链的支持最友好,许多编辑器直接内置JSON解析与校验能力,这也是它在轻配置场景里依然强势的原因。
4.3 JSON与XML、YAML的对比选型
为了搞清楚JSON在众多数据格式中的定位,把它和XML、YAML放在一起对比会更直观。
| 维度 | JSON | XML | YAML |
|---|---|---|---|
| 可读性 | 较好 | 差 | 好 |
| 学习成本 | 低 | 中 | 低 |
| 注释支持 | 不支持 | 支持 | 支持 |
| 数据类型表达 | 原生支持 | 弱 | 强 |
| 解析复杂度 | 低 | 高 | 中 |
| 工具链成熟度 | 极高 | 高 | 中 |
XML早期的Web服务(SOAP)里曾经是绝对主力,但它标签冗长,解析库普遍笨重,序列化性能也不占优。YAML可读性好、支持注释,但缩进规则一旦写错就很恼人,而且反序列化过程中如果遇到类型猜测不可控,在安全性要求高的场景里是一个隐患。JSON正好站在两者中间:足够简单、类型明确、解析器普遍轻快、短时间内不会退出历史舞台。
选择哪种格式,取决于项目环境。如果写配置且希望加注释,用YAML;如果历史系统传数据用的是XML,那就维持XML;但如果项目从零开始,特别是前后端分离的Web开发,JSON几乎是默认选项。
4.4 大规模数据交换的限制与对策
JSON在大规模数据交换场景里也有短板:文本格式冗余较大,没有二进制格式的压缩率;解析速度天然不如Protobuf这类二进制协议。
在性能和存储要求较高的场景,比如微服务内部调用、通信网关上报数据,我见过不少团队直接从JSON迁移到Protobuf,体量大的数据使用后网络带宽消耗明显下降。不过在数据量没有大到离谱、开发团队规模较小的时候,为了Protobuf多引入一套IDL编译流程不一定划算。
折中方案也有:保留JSON作为人与系统之间的接口格式,但底层传输采用压缩方式,比如启用Gzip。我实测过,对无缩进的JSON字符串启用Gzip压缩后,体积通常能压缩到原来的十分之一左右。换句话说,在数据量还停留在“兆”这个量级时,JSON配合压缩完全够用。
5. 动手实操:从构造JSON到命令行解析
5.1 手写JSON时的标准动作
直接手工构造JSON时,我建议按下面这个顺序走一遍,减少低级错误。
先确定根类型。是对象还是数组?最外层的花括号或方括号,决定整个数据的容纳方式。多数情况下根类型是对象。
然后逐层填充键值对。写一个键,紧跟冒号,再写字面量值,逗号结尾(最后一对不加)。书写时如果嵌套结构复杂,先把结构骨架写出来,再往里面填实际数据,不要图省事一次性写完整个几十行的JSON。
写完以后,立刻做一次静态校验。最初的几年里我习惯用在线校验工具粘贴检查,后来发现命令行更顺手——python -m json.tool可以直接从终端检查JSON合法性,还能格式化输出。比如:
echo '{"name":"张三","age":28}' | python3 -m json.tool如果JSON合法,它会输出格式化后的多行版本;如果不合法,会直接在报错行附近给出提示。这个命令在Linux和macOS上都能用,足够应付日常手工校验需求。
5.2 多语言解析与序列化核心代码
JSON相关的核心操作集中在两个动作:解析(字符串变内存对象)和序列化(内存对象变字符串)。不同编程语言叫法略有不同,但思路一致。
Python使用标准库:
import json # 序列化:dict -> JSON字符串 data = {"name": "张三", "scores": [90, 85, 92]} json_str = json.dumps(data, ensure_ascii=False, indent=2) print(json_str) # 解析:JSON字符串 -> dict parsed = json.loads(json_str) print(parsed["name"])ensure_ascii=False这个参数很关键。默认情况下Python会把非ASCII字符转成Unicode转义序列,输出显示成\u5f20\u4e09,不方便调试。设成False后中文能直接显示。
JavaScript则更直接:
// 序列化 const data = { name: "张三", scores: [90, 85, 92] }; const jsonStr = JSON.stringify(data, null, 2); console.log(jsonStr); // 解析 const parsed = JSON.parse(jsonStr); console.log(parsed.name);当JavaScript的JSON.stringify遇到不支持的属性值(比如undefined或函数)时,会自动丢弃这些属性。如果不希望某些敏感字段被序列化,我们可以向stringify传入第二个参数,它是一个数组或回调函数,专门用来指定哪些字段参与序列化。
Java生态中,如果使用Jackson,可以配置ObjectMapper来开启格式化输出:
ObjectMapper mapper = new ObjectMapper(); mapper.enable(SerializationFeature.INDENT_OUTPUT); String json = mapper.writeValueAsString(data); MyType obj = mapper.readValue(json, MyType.class);以上几种写法覆盖了绝大多数开发场景。一个值得养成的习惯是:在代码里尽量使用各语言官方或事实标准的JSON库,不要自己写字符串拼接逻辑。自己拼接轻则引号漏写,重则导致API调用方的解析直接崩溃。
5.3 命令行工具jq的高频用法
如果经常要处理JSON格式的日志或接口返回,我强烈建议学会jq。这是命令行处理JSON的神器,掌握几个基础用法就足以应对八成以上的日常场景。
从接口或文件读取并格式化JSON:
curl -s https://xxx.example.com/api/data | jq '.'提取指定键的值:
echo '{"name":"张三","age":28}' | jq '.name'输出:
"张三"如果想过滤数组里的元素,可以配合select。比如从一个接口返回的列表里找出所有状态为“成功”的记录:
cat response.json | jq '.data.list[] | select(.status == "success") | {id, name}'这条命令会把对象数组展开,逐条判断,符合条件的则提取出id和name字段。比起打开几百行JSON用肉眼找,效率完全不是一个级别。
jq还有一些高级用法,比如把JSON转成CSV、按字段排序、分组统计等。若你常在终端工作,把jq当作日常工具箱的一部分,你会发现处理数据不再那么烦躁。
5.4 用JS模拟深度解析流程的示例代码
为了加深对JSON解析过程的理解,我用JavaScript实现一个简化版的解析函数,这个例子不适合直接用于生产环境,但能清晰展示“从文本到对象”的还原过程:
function parseValue(str, index) { if (str[index] === '{') return parseObject(str, index); if (str[index] === '[') return parseArray(str, index); if (str[index] === '"') { let end = str.indexOf('"', index + 1); return { value: str.slice(index + 1, end), next: end + 1 }; } // 处理数字、true、false、null 的简化逻辑 let end = index; while (end < str.length && str[end] !== ',' && str[end] !== '}' && str[end] !== ']') end++; let raw = str.slice(index, end).trim(); if (raw === 'true') return { value: true, next: end }; if (raw === 'false') return { value: false, next: end }; if (raw === 'null') return { value: null, next: end }; return { value: Number(raw), next: end }; } function parseObject(str, index) { let obj = {}; index++; // 跳过 { while (str[index] !== '}') { while (str[index] === ' ' || str[index] === ',') index++; let keyRes = parseValue(str, index); let key = keyRes.value; index = keyRes.next; while (str[index] === ' ' || str[index] === ':') index++; let valRes = parseValue(str, index); obj[key] = valRes.value; index = valRes.next; } return { value: obj, next: index + 1 }; } function parseArray(str, index) { let arr = []; index++; // 跳过 [ while (str[index] !== ']') { while (str[index] === ' ' || str[index] === ',') index++; let valRes = parseValue(str, index); arr.push(valRes.value); index = valRes.next; } return { value: arr, next: index + 1 }; }这段代码忽略了很多边界细节,比如字符串转义、多层嵌套的终止条件,但它把“递归下降”的核心逻辑体现出来了——解析器每次只处理一个值,遇到对象或数组就递归进入内部处理。理解这个过程,再看JSON字符串里那些容易出错的地方,比如多一个逗号、少一个花括号,就能直观明白为什么字符串可以“看起来挺对”而解析器却报错。
6. 常见问题与排查技巧实录
6.1 解析器给出的错误提示不够直观怎么办
JSON解析报错信息在不同语言里差别很大。有的库只告诉你Unexpected token在第几行第几列,有的直接报一个笼统的位置。遇到提示不够直观的情况,我的做法是先定位出错位置的行列,把那一段内容复制出来,单独贴到一个干净的文本文件里对照检查。
最常见的报错原因排序如下:
- 末尾多了一个逗号(比如
"age": 28,}) - 字符串少写了一个引号(导致后续结构被吞进字符串)
- 键名没有用双引号包裹(有些可视化工具会自动纠错,但标准JSON不允许)
- 前导零
- 控制字符没有转义
当一行一行找不出问题时,把JSON内容去掉所有换行和缩进,压成一行,再检查首尾括号是否配对。这个方法看着蠢,但实际排查效率很高。
6.2 部分字段缺失或类型不符的运行时问题
有些情况下JSON语法是合法的,但数据结构不符合预期。比如后端返回的字段名大小写不一致,或者某个字段该是字符串却返回了数字,这会让前端代码在运行时出现undefined或类型错误。
处理这类问题,我不会直接在前端代码里到处加容错,而是先要求后端把字段名和类型统一。这属于接口契约问题。在联调阶段,把接口返回的JSON存下来,用像JSON Schema这样的校验工具做一次完整校验,能提前暴露字段缺失和类型不符。JSON Schema单独展开是一个大话题,这里你只需要知道:它就是一种描述JSON结构的规则,可以告诉一个JSON文件该长什么样,哪几个字段必须有、值必须是什么类型。为了解决“JSON语法对但业务对不上”的痛点,这是目前最标准的办法。
6.3 特殊字符到底要不要转义:一个实战决策过程
有一次我在某系统里写一个包含多行文本的JSON配置,文本里有换行和双引号。我准备手工把换行符替换成\n,把双引号替换成\"。另一个同事选择用程序里的JSON库先序列化再粘贴,这样所有转义都是自动处理的。
这个分歧反映了写JSON的两种姿态:手工拼写省事但容易遗漏;程序生成虽然多几步,但能保证正确。以我的经验,程序库生成的JSON偶尔会对某些字符做不期望的转义(比如把中文转成Unicode序列),可它绝不会生成非法的JSON。因此凡是超过十行的JSON内容,我几乎都是让代码或脚本生成,不做手工拼接。这个习惯帮我省下了大量排查时间。
6.4 大文件JSON的读取瓶颈与流式解析
很多人在读取几百MB的JSON文件时,直接用JSON.parse或json.loads一把梭,然后内存直接飙升。对于超大JSON,正确的思路是流式解析。
Python里可以和ijson这类流式库配合使用:
import ijson with open("big_data.json", "rb") as f: for item in ijson.items(f, "data.item"): process(item)这种方式不是一次性把整个JSON载入内存,而是边读边处理,对于数据量特别大的文件几乎是唯一可行的方案。
如果暂时不想引入第三方库,也有折中做法:先用命令行工具把大文件拆成多个小文件,再逐个处理。大多数情况下,拆开以后的内存占用会改善到合理范围内。
6.5 前后端联调时的终极定位法
联调中前端说后端返回的JSON有问题,后端说自己的数据没问题,这种互相推诿的场景几乎每个开发团队都碰过。我总结了一个定位顺序,能快速缩小范围:
首先打开浏览器的网络面板,查看原始响应体,确认后端真正返回的JSON是什么样子。很多前端框架会在请求层做统一拦截和二次包装,前端拿到手的对象和后端原始字符串可能已经不一样了。接着复制原始响应文本,用命令行或者在线工具做语法校验,排除网络传输过程中被截断、被二次转义的可能。最后再判断是数据内容的问题,还是前端解析逻辑的问题。
整个过程的核心就是:先锁定“原始数据到底长什么样”,再做后续判断。很多看似是JSON的问题,最后发现其实是中间层对字符串做了处理,比如后端把JSON包在了一个普通字符串字段里,或者HTTP响应头声明的字符集与内容不一致。先看响应原始内容,这个问题就能一次性看穿。
7. 在项目中建立JSON使用规范
经常处理JSON之后,我意识到一个问题:JSON不存在“会不会写”的门槛,但团队协作中“写得好不好”会实实在在影响开发和维护效率。所以我建议在项目里建立一个精简的JSON使用规范,不用太长,三五条就够。
字段命名统一。选下划线还是小驼峰,定一个就好,最好在接口文档里顺手写下。涉及集合统一用数组表达,避免出现user_list和users混用的情况。时间字段统一用ISO 8601字符串,带时区,不要今天返回时间戳明天返回字符串。数字ID统一转字符串,防止大数精度丢失。响应外层统一包裹状态码和消息字段,即便接口不需要提示消息,也保留这一个结构,这样前端的拦截器逻辑可以统一处理。
当初我把这些规则讲给配合的初级开发者以后,联调阶段的返工率明显下降。很多时候问题不出在某个人的能力,而是缺少共同遵守的约定。
8. 最后分享几个我在实际项目中积累的JSON排查小工具与习惯
最后一个部分不打算总结一遍前文,而是分享几个我长期坚持的习惯,它们不是标准教程里会教的内容,但确实省了我不少力气。
第一个习惯是在本地常备一个格式化脚本。我用Python只写了三行:
import sys, json print(json.dumps(json.load(sys.stdin), ensure_ascii=False, indent=2))起名为 fmtjson.py,放进PATH。任何时候拿到一段被压缩到一行的JSON,直接cat data.json | fmtjson,马上变成可读的多行格式。同样的思路也可以做一个压成一行去文件对比的逆向脚本,用来做接口返回的diff。
第二个习惯是在提交代码前,对所有改动里出现的JSON字符串做一次静态校验。我见过不少项目,JSON字符串里藏着一个尾逗号,白天改代码时完全没注意,晚上发布后程序启动失败才暴露出来。用脚本批量扫描项目中的.json文件和代码里的字符串常量,做一个合法性检查,几十秒就能完成。这个动作我已经养成肌肉记忆了。
第三个习惯是善用浏览器的开发者工具控制台。前端联调时,把接口返回字符串在控制台跑一次JSON.parse,任何语法错误都能即刻显示行号。配合上断点,几乎能锁定到具体字段。
JSON看起来简单,但“简单”不代表“不重要”。我们在日常开发里遇到的大部分接口异常,都能回到JSON本身去找线索。把上面这些规则和习惯内化了以后,再碰到JSON相关的问题,大概率会在几分钟内找到根因。如果哪天你在调试一个玄学问题,查来查去毫无头绪,不妨先回头把链路里所有经手过JSON的环节重新捋一遍——很多时候答案就藏在那看起来平平无奇的括号和引号里。