前阵子接了个老项目的维护,第三方接口返回的JSON嵌套了四层,里面数组套对象、对象套数组,前端同事写了一大坨遍历逻辑提取数据,每次接口结构调整都要跟着改代码,改到后来他自己都晕了。我过去看了一眼,跟他说这东西就别傻傻地手动解析了,一条JSONPath表达式就能把数据提出来,结构变了最多改一行匹配规则。他听完实验了一把,回头跟我感叹:这玩意儿怎么没早点知道。
如果你也在写接口对接、写爬虫、做测试断言、处理配置文件,或者每天跟各种API返回的数据打交道,那“JSON + JSONPath”这对组合绝对是值得花半小时吃透的基础功。JSON负责数据的组织与传输,JSONPath负责从深层JSON里精准捞数据,两者搭配使用,能省掉大量手写遍历的重复代码。这篇文章我会从JSON最基本的格式规范讲起,再重点拆解JSONPath的语法和实战用法,最后把高频报错的排查思路一起整理出来,前后端、测试脚本、数据处理这些场景都能直接用上。
1. 先把基本功打牢:JSON 格式与数据模型
1.1 JSON到底是个什么东西
JSON(JavaScript Object Notation)虽然名字里带着JavaScript,但它早就不属于JavaScript了,而是一种跨语言的数据交换格式。后端服务给前端返回数据、两个系统之间做接口通信、软件存配置文件、数据库存半结构化字段,到处都能看到JSON的身影。它之所以这么普及,靠的是三个优势:对人可读、对机器易解析、几乎所有编程语言都内置了支持。
拿咱们最常用的场景来类比:XML像是写论文时用的严格格式,标签开头结尾必须配对,信息完整但啰嗦;YAML像是写便签,简洁好看但对缩进极其敏感,一个空格错了就解析失败;而JSON更像是填表,结构清晰,字段名和值一一对应,不搞花活。所以现在绝大多数Web API都默认返回JSON,新工具链也基本把JSON当作配置格式的首选。
1.2 语法规则与六种数据类型
JSON的数据模型并不复杂,它只有六种值类型:对象(object)、数组(array)、字符串(string)、数字(number)、布尔值(true/false)、空值(null)。其中对象和数组是复合结构,可以任意嵌套,字符串、数字、布尔、null是叶子节点,负责存具体数据。
一个典型的订单数据结构长这样:
{ "orderId": "A10086", "userId": 9527, "amount": 199.5, "paid": true, "remark": null, "items": [ { "sku": "SKU-001", "name": "无线鼠标", "price": 99.75, "count": 2 } ] }这里有几个语法红线,新手特别容易踩:
- 键名必须用双引号包裹,单引号不行,裸键名也不行。
- 字符串必须用双引号,普通文本内容里如果包含双引号,需要转义为
\"。 - JSON不支持注释,不要想着写
//或者/* */进去。 - 数组和对象的最后一个元素后面不能加逗号,也就是不能有“尾逗号”。
- 数字不能写成
01这种前导零格式,也不能出现NaN、Infinity。
我见过很多线上问题都是从一个多余的逗号开始的,尤其是当JSON文件由多个部分拼接生成时,漏掉一个逗号或者多出一个逗号,整个文件就解析失败。建议养成的习惯是:只要是手工维护的JSON,写完一定过一遍格式校验工具。
1.3 从“一行”到“竖排”:格式化与常用工具
很多接口返回或者日志里的JSON是压缩成一行输出的,机器读起来没问题,人眼看起来就是灾难。“vs json文件全部在一行”这种需求,本质上就是把压缩的JSON格式化(也叫美化、pretty print)成带缩进的可读形式。
解决途径好几个,按场景选就行:
- VS Code里打开JSON文件,用快捷键
Shift+Alt+F直接格式化,这个操作内置支持,不用装插件。 - Notepad++可以装JSON Viewer插件,格式化之外还能在侧边栏以树形结构查看JSON层级。插件如果是离线环境,就下载对应版本的NPPJSON插件放到
plugins目录里,手动加载即可。 - 命令行环境用
jq,一句jq . data.json就能格式化输出,还能顺便做筛选。 - Python环境可以用标准库,
python -m json.tool data.json,简单直接。 - 如果只是临时看一下,在线工具也无妨,但要注意别把含敏感信息的JSON贴到不信任的网站上,公司项目更要谨慎。
格式化这件事我会多说一句:不要觉得这是小事,格式化后的JSON配合行号、缩进、括号高亮,排查问题时人脑处理效率能提升一大截。许多解析报错,格式化之后一眼就能看出括号少了半边。
2. JSONPath:给 JSON 写的查询语言
2.1 为什么要单独学一门查询语法
JSON本身只负责“存数据”,它不提供查询能力。平时要从一个嵌套很深的JSON里取数据,最常见的做法是什么?用代码一层层取:先取对象属性,再判断是不是数组,再循环遍历……代码写起来啰嗦不说,一旦数据结构层级变了,所有取值逻辑都得跟着动。
JSONPath就是来解决这个问题的。它相当于给JSON写的“SQL”,用一条表达式描述“我要的数据在哪、满足什么条件”,然后在JSON文档里匹配并提取出来。学到这个程度,你会发现很多数据结构上的奇技淫巧都变得透明了。
JSONPath最早由Stefan Goessner在2007年提出,设计上参考了XPath(XML的查询语言),所以熟悉XPath的人看JSONPath会有亲切感。2024年它正式成为了RFC 9535标准,也就是说现在市面上的JSONPath实现基本都有共同遵循的规范了。
2.2 核心语法速查
JSONPath的语法不算多,核心是几个符号的组合。为了好记,你可以把JSON的嵌套结构想象成一棵树,对象是树干,数组是树枝,叶子节点就是具体数据。JSONPath表达式的任务就是告诉程序“从哪个树干走到哪个树枝,甚至跨过树枝直达某片叶子”。
先把常用操作符一次性列清楚:
| 操作符 | 含义 | 举例 |
|---|---|---|
$ | 根节点,表达式的起点 | $.store表示根下的store |
.key或['key'] | 取对象的指定属性 | $.store.book |
[n] | 从数组取第n个元素,从0开始 | $.store.book[0] |
[*] | 通配符,匹配所有元素 | $.store.book[*] |
.. | 递归下降,跳过中间层级找所有匹配项 | $..price |
@ | 当前节点,通常在过滤表达式里用 | $.store.book[?(@.price < 10)] |
[?(条件)] | 过滤表达式,筛选符合条件的元素 | $.store.book[?(@.price < 10)] |
[start:end:step] | 数组切片,类似Python的切片 | $.store.book[0:2] |
[-n:] | 取数组最后n个元素 | $.store.book[-1:] |
这些语法组合起来,表达能力很强。不过有一点要注意:不同语言和不同第三方库对JSONPath的细节支持有差异,比如过滤表达式有的库要求用[?()],有的库支持[?()]但不支持单引号,有的库还额外支持|联合、~键名匹配等扩展操作。写之前瞄一眼你用的库的文档,能省不少纠结。
2.3 经典示例:图书商店查询实战
用一段经典的JSON示例数据来说明,会清晰很多。假设我们有这样一个书店数据结构:
{ "store": { "book": [ { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 }, { "category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99 }, { "category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "isbn": "0-553-21311-3", "price": 8.99 }, { "category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "isbn": "0-395-19395-8", "price": 22.99 } ], "bicycle": { "color": "red", "price": 19.95 } } }我们来逐个查询:
$.store.book[*].title:取所有书的标题。结果是四条title的数组。$..price:递归取所有price。你会拿到所有书和自行车的价格,共5个数字。$.store.book[1]:取第二本书(下标从0开始,所以1对应第二本)。$.store.book[-1:]:取最后一本书。$.store.book[?(@.price < 10)].title:取价格小于10的书的标题。这里@代表数组中的每一个元素,@.price就是当前这本书的价格,条件筛选后取title。
这些表达式基本覆盖了日常开发的90%场景:取某个字段、取列表、按条件过滤、递归找值。掌握了这些,JSON在代码里的“盲区”就基本不存在了。
3. 实操:在 Python 和 JavaScript 中跑通 JSONPath
3.1 Python:标准库 json + jsonpath-ng
Python标准库自带json模块,负责序列化和反序列化。JSONPath解析则需要安装一个第三方库,我用得比较多的是jsonpath-ng,它严格实现了JSONPath语法,还支持对匹配结果做修改。
安装:
pip install jsonpath-ng然后可以这样查上面的书店数据:
import json from jsonpath_ng import parse data = { "store": { "book": [ {"category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95}, {"category": "fiction", "author": "Evelyn Waugh", "title": "Sword of Honour", "price": 12.99}, {"category": "fiction", "author": "Herman Melville", "title": "Moby Dick", "price": 8.99}, {"category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "price": 22.99} ], "bicycle": {"color": "red", "price": 19.95} } } # 解析JSONPath表达式 expr = parse("$.store.book[?(@.price < 10)].title") matches = expr.find(data) for match in matches: print(match.value)执行这段代码,会打印出两本书的标题:Sayings of the Century和Moby Dick。expr.find(data)返回的是匹配对象列表,每个match.value就是实际取到的值。如果匹配不到任何东西,matches就是空列表,不会抛异常,这点在写脚本时很友好。
如果只是从接口返回的JSON字符串开始,就先用json.loads()把字符串转成Python对象,再丢给jsonpath_ng查。顺序是:文本 -> 对象 -> 查询。
3.2 JavaScript / TypeScript:JSON.parse + jsonpath-plus
前端原生环境里有JSON.parse(),可以直接把JSON字符串转成对象,但原生JavaScript并没有内置JSONPath。需要使用第三方库,我常用jsonpath-plus,它支持浏览器和Node环境,功能很全。
安装:
npm install jsonpath-plus使用示例:
import { JSONPath } from 'jsonpath-plus'; const data = { store: { book: [ { title: 'Sayings of the Century', price: 8.95 }, { title: 'Sword of Honour', price: 12.99 }, { title: 'Moby Dick', price: 8.99 }, { title: 'The Lord of the Rings', price: 22.99 } ], bicycle: { color: 'red', price: 19.95 } } }; const result = JSONPath({ path: '$.store.book[?(@.price < 10)].title', json: data }); console.log(result); // ['Sayings of the Century', 'Moby Dick']注意jsonpath-plus的JSONPath()函数接收对象参数,path是JSONPath表达式,json是要查询的数据对象。返回结果直接是一个数组,即使只匹配到一个元素,也会被包在数组里。这个特性和Python库不太一样,写代码时需要注意区分。
3.3 其他常见场景:Qt 读写、jq 命令行、MySQL JSON 字段
除了Python和前端,JSON相关的需求在工作中出现的频率也很高。我简单盘几个高频场景的用法:
Qt/C++读写JSON
如果是用Qt做桌面客户端或嵌入式开发,一般用QJsonDocument来解析:
#include <QJsonDocument> #include <QJsonObject> #include <QJsonArray> #include <QFile> QFile file("data.json"); file.open(QIODevice::ReadOnly); QByteArray bytes = file.readAll(); QJsonDocument doc = QJsonDocument::fromJson(bytes); QJsonObject root = doc.object(); QJsonArray books = root.value("store").toObject().value("book").toArray(); for (int i = 0; i < books.size(); ++i) { QJsonObject book = books[i].toObject(); qDebug() << book.value("title").toString(); }Qt没有内置JSONPath,但QJsonDocument提供的树形访问已经能覆盖大部分读写需求。真要复杂查询,可以引入jsonpath-ng类似功能的C++库,或者先用Python等脚本把数据预处理成扁平结构再交给Qt。
jq 命令行
在Linux服务器上排查数据,jq是神器。虽然jq的语法不是标准JSONPath,但思想一样:
cat data.json | jq '.store.book[] | select(.price < 10) | .title'这一句能做同样的事情,而且输出还能继续做管道处理。平时在终端里想快速看接口返回某个字段,curl xxx | jq '.data.list'比写脚本快太多。
MySQL JSON 字段
MySQL从5.7开始支持JSON类型,可以把半结构化数据直接存进表里,再用JSON函数查询:
-- 提取字段 SELECT profile->>'$.age' FROM users; -- 按JSON字段查询 SELECT * FROM users WHERE JSON_EXTRACT(profile, '$.age') > 18; -- 给JSON字段创建索引 ALTER TABLE users ADD INDEX idx_age ((CAST(profile->>'$.age' AS UNSIGNED)));->>是提取JSON字段并转为SQL字符串的语法,JSON_EXTRACT返回JSON类型。千万要注意:直接对JSON列创建普通索引是无效的,必须用CAST(... AS ...)这种生成列的方式,否则查询还是会走全表扫描。
4. 高频报错排查:那些年我们踩过的 JSON 坑
4.1 格式类错误:Unexpected end of JSON input / JSONDecodeError
先看三个高频格式报错,它们出镜率极高。
unexpected end of JSON input是Go语言里的典型报错,意思是JSON字符串还没结束就没了。比如:
var raw []byte = []byte(`{"name":"demo"`) var obj map[string]interface{} if err := json.Unmarshal(raw, &obj); err != nil { log.Fatal(err) }这段代码会直接报错,因为大括号没闭合。类似的,Python里会报JSONDecodeError: Expecting value或者Expecting property name enclosed in double quotes。JavaScript的JSON.parse()会报Unexpected end of JSON input或Unexpected token。
这类错误九成发生在以下场景:
- 接口返回被截断了,网络问题或者服务端超时,只收到一半数据。
- 日志系统自动截断字段,存到文件里JSON不完整。
- 手工拼JSON字符串时,漏了后半个括号。
- 多个JSON片段拼接时,中间少了逗号或者括号没配对。
排查方法很简单:把报错的JSON文本拷贝出来,用格式化工具过一遍,看到哪一行缩进或者括号对不上,问题就在哪。如果数据量特别大,可以写个简单的Python脚本,逐个字符扫描括号配对,快速定位位置。
4.2 Python 中 TypeError: Object of type set is not JSON serializable
这是Python特有的坑。Python的数据类型比JSON丰富,set、datetime、bytes、Path这些类型都不能直接被json.dumps()序列化。比如:
import json data = {"tags": {"python", "json", "tips"}} json.dumps(data)直接报错:TypeError: Object of type set is not JSON serializable。原因很简单:JSON标准里没有“集合”这个概念,只有数组。Python的list对得上JSON的数组,set对不上。
解决办法:
import json data = {"tags": {"python", "json", "tips"}} data["tags"] = list(data["tags"]) # 如果数据里有datetime,用default参数统一处理 from datetime import datetime data["update_time"] = datetime.now() json.dumps(data, ensure_ascii=False, indent=2, default=str)default=str的意思是:碰到不知道怎么序列化的对象,就调用str()把它转成字符串,这个技巧处理datetime、Decimal特别方便。不过要注意,转成字符串之后,下游解析时就需要自己再转了。
4.3 Rust 的 missing field 报错与结构体映射
再来看一个后端开发高频报错:failed to deserialize the json body into the target type: input: missing field。这是Rust生态里serde库的经典报错,意思是:目标结构体里声明了一个必填字段,但输入的JSON里没有这个字段。
比如定义了一个订单结构体:
#[derive(serde::Deserialize)] struct Order { order_id: String, total_amount: f64, }请求体只传了{"order_id": "A10086"},serde反序列化时就会报missing field total_amount。这类报错几乎都是接口联调时字段名对不上导致的。排查方向有这么几个:
- 请求JSON里的字段名拼写,跟结构体字段是否完全一致。
- 字段名大小写是否匹配,很多JSON字段是下划线风格(snake_case),Rust结构体默认也是snake_case,但如果接口那边用的是驼峰(camelCase),就需要用
#[serde(rename_all = "camelCase")]来标注。 - 某些字段允许缺失的话,类型要设为
Option<String>,并且在结构体上标注#[serde(default)]。
还有一类常见情况是类型不匹配。比如接口返回"count": "12"(字符串数字),但结构体里声明的是i32,serde也会反序列化失败。必要时用#[serde(deserialize_with = "...")]写一个自定义反序列化函数,把字符串数字转成数字。
4.4 数组处理、中文编码与映射错误
JSON数组的处理
接口返回的最外层经常是数组,比如[{"name": "Alice"}, {"name": "Bob"}]。Python里用json.loads()后直接得到list,遍历即可。但很多人容易忽略的是:数组里每一项都是独立对象,如果取其中某个字段,要先遍历还是直接索引,取决于业务需求。用JSONPath的话,$[*].name一下就把所有name取出来了,比手写循环简洁得多。
中文编码乱码
json.dumps()默认会将非ASCII字符转义成\uXXXX形式,比如“中文”变成"\u4e2d\u6587"。这其实没错,JSON标准允许,很多接口也会这么做。但如果想让文件里直接看到中文,加一个参数:
json.dumps(data, ensure_ascii=False, indent=2)无法将JSON输入源映射到目标字段
这类报错出现在低代码平台或配置型集成工具里,原文类似“无法将 json 输入源 /body/total_amount 映射到目标字段转账总金额中”。这本质上还是字段映射问题:源JSON里/body/total_amount这个路径找不到,或者类型对不上。排查思路是:先确认源数据里字段的真实路径和真实类型,再看目标字段的映射配置是否写对了路径,最后看有没有类型转换步骤。特别是金额字段,源是字符串还是数字,目标有没有做精度要求,都要一一核对。
4.5 格式对比与结构变更
接口升级后经常出现JSON结构变了、老的解析代码全部失效的情况。我建议养成一个习惯:每次接口变更后,把新旧JSON各存一份,用在线工具或Beyond Compare做结构对比,看看哪些字段删了、哪些字段改了类型。相比人眼盯着一大段JSON看,对比工具能高亮差异,效率翻倍。
JSON数据对比对于测试人员来说尤其重要。接口自动化测试里断言某个字段是否符合预期,用JSONPath提取目标值再做断言,比做全量JSON相等断言稳定得多——因为全量断言只要新增一个字段就失败,而JSONPath只关心你要验证的那几个点。
5. 进阶玩法:JSON Schema、大模型结构化输出与 JSON RPC
5.1 用 JSON Schema 给数据上“枷锁”
前面讲的所有内容都默认JSON数据是对的,但现实是:别人给你的接口返回经常不按文档来。这时候就需要在消费数据之前加一道校验关卡,JSON Schema就是干这个的。
JSON Schema本身也是一个JSON文件,用来描述数据结构和校验规则。看一个用户信息的Schema:
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "required": ["username", "email", "age"], "properties": { "username": { "type": "string", "minLength": 3, "maxLength": 20 }, "email": { "type": "string", "format": "email" }, "age": { "type": "integer", "minimum": 0, "maximum": 150 } } }用这个Schema去校验实际数据,username太短、email格式不对、age缺失或超范围,都能报出来。对于“deepseek json schema报错”这类问题,其实多半就是模型生成的数据没满足你给定的Schema约束,解法是:把Schema写得更具体、在Prompt里明确要求必须严格按Schema输出、输出后自动校验不通过就重新生成。
JSON Schema的好处是把“数据长什么样才算合法”从业务代码里抽离出来,变成一份可配置、可版本化的声明文件。接口联调时,双方约定好Schema,等于提前把数据契约定死了,比口头文档靠谱得多。
5.2 大模型应用里的 JSON 输出约束
现在大模型应用非常多,很多LLM接口允许你指定返回格式,让模型输出JSON,方便程序直接消费。比如让模型从一段文本中抽取结构化信息,你可以在Prompt里明确要求“只返回JSON,不要包含任何其他解释文字”,并附上一段JSON Schema作为参考。
不过实测下来,模型的JSON输出不能百分百保证合法。常见问题有:
- 模型在JSON外面包了
json代码块标记。 - 字段名不是双引号。
- 文本内容里出现未转义的控制字符。
- 生成了多余的逗号或者缺少右括号。
我的处理策略是三层兜底:
第一层,在Prompt里写清楚约束条件,并给一个负例提醒“不要添加注释、不要使用单引号”。第二层,拿到返回文本,先做一次JSON.parse()或json.loads(),解析成功就直接用。第三层,解析失败时,用正则或者简单修复规则清理掉代码块标记、多余逗号,再尝试二次解析;二次还不行就重试请求,让模型重新生成。
如果使用大模型做数据分析,大模型json这类需求会越来越多,掌握这套“约束输出 + 解析 + 修复 + 重试”的链路是很有必要的。
5.3 JSON RPC:一种很轻的接口协议
除了RESTful API,JSON还支持一种非常轻量的远程调用协议——JSON RPC。它的核心思想是:把方法名和参数用JSON封装起来,通过HTTP或其他传输层发出去,对方用JSON返回结果。
一个标准的JSON RPC 2.0请求:
{ "jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1 }对应的响应:
{ "jsonrpc": "2.0", "result": 19, "id": 1 }id字段用于关联请求和响应,params可以是数组也可以是对象,result和error二者必填其一。相比REST,JSON RPC少了HTTP动词、状态码、资源路径这些概念,接口定义更加集中,特别适合内部系统之间的轻量调用、浏览器插件与本地服务通信、以及一些去中心化应用场景。
6. 实战案例:省市区数据查询与远程配置场景
6.1 用 JSONPath 从省市区数据中提取目标地区
“省市区三级联动”数据是前端经常要用到的公开JSON数据集,结构通常是省套市、市套区,嵌套三层。这种数据拿来做JSONPath练习特别合适。
简化示例:
[ { "name": "广东省", "children": [ { "name": "广州市", "children": [ { "name": "天河区" }, { "name": "越秀区" } ] }, { "name": "深圳市", "children": [ { "name": "南山区" }, { "name": "福田区" } ] } ] } ]想一次性取出“广东省”下属所有城市,用JSONPath:
$[?(@.name == '广东省')].children[*].name解析流程是:先看根节点是一个数组,用[?(...)]过滤出name等于“广东省”的对象,再取它的children,最后取每个子节点的name字段。
想递归找出所有区一级的地名,不管它属于哪个市:
$..children[?(@.children == null)].name这里利用递归下降..跳过中间层级,先找到所有children数组,再用过滤条件筛出没有下级子节点的对象(即叶子节点),然后取name。这个模式在处理任意多层级树形数据时非常通用,像组织架构、分类树、评论楼中楼都能用。
6.2 远程配置地址与 JSON 资源的分发模式
现在很多工具类应用都流行“远程配置”的模式:应用启动时从一个URL拉取JSON文件,里面保存了最新的资源列表、功能开关、界面配置等,本地不硬编码这些信息,方便运营随时更新。
举一个比较通用的例子,某个应用的配置文件可能是这样的结构:
{ "version": "2026.01.15", "update_url": "https://example.com/latest.json", "groups": [ { "name": "默认分组", "entries": [ { "name": "条目A", "url": "https://example.com/a" }, { "name": "条目B", "url": "https://example.com/b" } ] } ] }应用启动时用HTTP库拉取这个JSON,再用JSONPath快速提取自己关心的部分,比如取第一个分组下的所有条目$.groups[0].entries[*],或者直接根据version字段判断要不要重新加载。
这种“JSON地址同步配置”的模式本身不复杂,核心就是把数据源和代码逻辑解耦。开发时还要注意一个问题:远程JSON的schema可能会变化,所以要给解析增加容错,至少要判断关键字段是否存在,别因为配置缺失直接把应用搞崩溃。
6.3 从命令行到脚本,JSONPath 的调试速度优势
最后分享一个我自己工作中的调试套路。拿到一个陌生接口的返回后,我不会急着写代码,而是在命令行里先用curl把返回存到本地文件,然后用jq或写一个临时Python脚本快速探索JSON结构。几个高频操作:
jq 'keys' data.json:看根节点有哪些字段。jq '.data | type' data.json:判断某个字段的类型。jq '.data.list[] | {name, id}' data.json:只输出需要的字段。
等确认了数据结构和需要的字段,再把这个JSONPath/jq表达式直接搬进正式代码里,用语言对应的JSONPath库实现。整个流程从“看到接口”到“跑通代码”基本在几分钟内完成,比边写代码边用print调试JSON要舒服得多。
在实际项目中我发现,JSONPath最容易被低估的场景是测试断言。接口自动化测试里,与其解析整个响应体再层层取字段,不如直接用JSONPath表达式定位目标值,断言表达式的结果等于期望值。一旦接口结构微调,只需要更新表达式,不需要重写解析逻辑,维护成本低很多。
就我个人经验来说,真正写好JSONPath表达式的一个关键是“先在脑子里把JSON结构画成树,再决定用点号路径还是递归下降”。点号路径适合层级明确的静态结构,递归下降适合那些“我也不知道它在第几层、反正叫这个名字”的字段。这两者配合过滤器,基本能应对所有数据结构。最后再提醒一句:JSON规范化这件事越早做越好,不管是写脚本还是写接口,养成先格式化再动手的习惯,能帮你省下不少排查时间。