JSONPath 语法完全指南:查询与过滤 JSON(含示例)
JSONPath 是 JSON 的查询语言,就像 XPath 之于 XML。你写一条路径表达式,求值器就会返回所有匹配的值。想从一份书店文档里取出全部作者名,只要写 $.store.book[*].author,作者列表就到手了,一行遍历代码都不用写。
本文会用可复制的示例把 JSONPath 语法里的每个选择器讲一遍,你可以边读边运行。有一点先说清楚:JSONPath 有两种方言。2007 年的 Goessner 方言是事实上的经典版,RFC 9535 则是 2024 年 2 月发布的 IETF 正式标准。它们在常见路径上一致,在边界情形上不同,本文会在差异出现的地方标出来。下面每条表达式你都能在 JSONPath 测试器里运行,并在两个引擎间切换对比。
先放一张选择器速查表,正文后面会用同一份 JSON 文档把每一行展开成可运行的示例。
| 选择器 | 含义 | 示例 |
|---|---|---|
$ | 文档根节点 | $ |
@ | 当前元素(filter 内) | [?(@.price < 10)] |
.name / ['name'] | 子成员 | $.store.book |
.. | 递归下降 | $..author |
* | 所有元素 / 成员 | $.store.book[*] |
[0] | 数组索引 | $.store.book[0] |
[start:end:step] | 数组切片(左闭右开) | $.store.book[0:2] |
[a,b] | 名称 / 索引的并集 | $.store.book[0,2] |
[?()] | 过滤表达式 | $.store.book[?(@.price < 10)] |
length() count() match() search() value() | RFC 9535 函数(仅限 filter) | [?length(@.title) > 15] |
JSONPath 是什么?
JSONPath 是一种声明式查询语言,用来从 JSON 文档里选取节点。你不必写一个循环去遍历对象和数组,而是用一条路径描述想要的位置,求值器就返回匹配的值。它的思维模型和 XPath 之于 XML 完全一样:一条由选择器组成的路径,逐级穿过结构。
凡是开发者接触 JSON 的地方,它都会出现。你用它从 API 响应里取一个字段,在集成测试里对某个值做断言,在 Kubernetes、AWS Step Functions、Azure Logic Apps 的流水线配置里寻址字段,以及从庞大或不规则的 JSON 里提取数据而不用手写遍历逻辑。
简单交代一段历史,因为它解释了方言为何分裂。Stefan Goessner 在 2007 年提出了 JSONPath。它传播很快,成了事实标准,但从未被正式规范化,于是各实现在细节上渐渐走偏。IETF 在 2024 年 2 月用 RFC 9535 补上了这个缺口,这是首个正式的 JSONPath 规范。如今两种方言都还在用,这就是同一条表达式在不同库里行为可能不同的原因。
开始查询前,先把结构看清楚会更顺手。用 JSON 格式化工具把杂乱的输入美化一下,嵌套层次就一目了然了。
示例文档
下面每个示例都基于经典的 Goessner 书店 JSON。粘贴一次,反复复用:
{
"store": {
"book": [
{ "title": "Sayings of the Century", "author": "Nigel Rees", "price": 8.95 },
{ "title": "Sword of Honour", "author": "Evelyn Waugh", "price": 12.99 },
{ "title": "Moby Dick", "author": "Herman Melville", "price": 8.99 },
{ "title": "The Lord of the Rings", "author": "J. R. R. Tolkien", "price": 22.99 }
],
"bicycle": { "color": "red", "price": 19.95 }
}
}
四本书,各有标题、作者、价格,外加一辆自行车。记住这一点:价格分别是 8.95、12.99、8.99 和 22.99,这会决定后面过滤的结果。
根、子节点与递归下降($ . ..)
每条表达式都从根开始,写作 $。从根出发,你用点号或方括号写法进入子节点,两者等价:
$.store.book → the book array
$['store']['book'] → identical result
当键里含有空格、点号或其他特殊字符时,就得用方括号写法:$['first name'] 行得通,而 $.first name 不行。
.. 是递归下降(recursive descent)运算符。它搜索文档的每一层,而不只是直接子节点,并把任意深度上匹配后续选择器的内容全部收集起来:
$..author
→ ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "J. R. R. Tolkien"]
何时用 ..,何时写全路径
递归下降很省事,但也很粗放。$..price 会匹配树里任何位置的 price,包括 store.bicycle.price,而这可能并不是你想要的。当你清楚数据形状时,就把路径写全,让查询保持精确:
$..price → [8.95, 12.99, 8.99, 22.99, 19.95] (includes the bicycle)
$.store.book[*].price → [8.95, 12.99, 8.99, 22.99] (only books)
把 .. 留给真正不规则或未知的结构。这里要在便利和控制之间权衡:你对数据了解得越多,就越应该选择显式路径。
通配符、索引与数组切片(* [0] [start:end:step])
通配符 * 选取数组的所有元素,或对象的所有成员:
$.store.book[*].title
→ ["Sayings of the Century", "Sword of Honour", "Moby Dick", "The Lord of the Rings"]
数组索引从零开始,负索引从末尾倒数:
$.store.book[0].title → ["Sayings of the Century"]
$.store.book[-1].title → ["The Lord of the Rings"]
切片采用和 Python、JavaScript 相同的左闭右开(half-open)[start:end:step] 约定:start 包含在内,end 排除在外。
$.store.book[0:2].title → ["Sayings of the Century", "Sword of Honour"]
它返回的是两本书,不是三本:索引 0 和索引 1,到索引 2 之前停下。右端排除是 JSONPath 里最常见的一个坑,记牢它:
[0:2] → first TWO elements (indices 0, 1) ← correct
[0:3] → first THREE elements (indices 0, 1, 2)
省略某个边界可以一直取到尽头,加一个步长则可每隔 N 个取一个:
$.store.book[2:].title → ["Moby Dick", "The Lord of the Rings"]
$.store.book[:3].title → first three titles
$.store.book[::2].title → ["Sayings of the Century", "Moby Dick"] (every other)
过滤表达式 [?()]——最强大的部分
过滤器是 JSONPath 真正发挥价值的地方。过滤器 [?()] 只保留谓词为真的元素,而在过滤器内部,@ 指代当前正在测试的元素。
要选出价格低于 10 的书:
$.store.book[?(@.price < 10)].title
→ ["Sayings of the Century", "Moby Dick"]
对照书店价格(8.95、12.99、8.99、22.99),有两本书符合。下面是一步步构建过滤谓词的方法:
- 与字面量比较。 使用
==、!=、<、<=、>、>=,例如@.price > 10。 - 匹配字符串。 字符串字面量用单引号:
@.author == 'Nigel Rees'。 - 测试是否存在。 一个裸成员引用会选出含有它的元素:
[?(@.isbn)]只保留有isbn的书。 - 组合条件。 用
&&和||连接多个谓词:[?(@.price < 10 && @.author == 'Herman Melville')]。
最常见的过滤错误是 @ 的作用域。在谓词内部,当前元素是 @,不是 $。写成 $.price 会指回文档根,而不是正在测试的那本书:
$.store.book[?($.price < 10)] → wrong scope, matches nothing useful
$.store.book[?(@.price < 10)] → correct: each book's own price
filter 中的 RFC 9535 与 Classic 差异
两种方言在空白与引号上的处理不同。Classic 很宽松,[?(@.price<10)] 不带空格也能正确解析。RFC 9535 严格遵循自己的语法,对过滤器的写法要求更严。如果某个在别处能用的过滤器失败了,检查一下空格和引擎。把过滤器写干净(运算符两侧留空格、字符串用单引号),无论最终是哪个库来运行,结果都一致。
并集选择器——一次选取多个键([a,b])
并集选择器在一个方括号里列出多个名称或索引,把它们全部取出:
$.store.book[0]['title','author']
→ ["Sayings of the Century", "Nigel Rees"]
并集对索引同样适用,你也可以把它和其他选择器混用,做固定字段投影:
$.store.book[0,2].title → ["Sayings of the Century", "Moby Dick"]
$.store.book[*]['title','price'] → title and price of every book
当你想要的是几个特定字段,而不是整个对象或一次通配扫描时,并集就是合适的工具。
RFC 9535 函数:length、count、match、search、value
RFC 9535 定义了五个标准函数扩展。有一条规则几乎人人都会栽,竞品指南也经常写错:
这些函数只能在过滤器
[?...]内调用,绝不能作为独立的路径段。
写 $.store.book.length() 不是有效的 RFC 9535,标准语法会拒绝它。那种段调用形式是 jsonpath-plus 的扩展,并非规范的一部分。要按长度过滤,你得在谓词内部调用该函数:
$.store.book[?length(@.title) > 15]
→ [
{ "title": "Sayings of the Century", "author": "Nigel Rees", "price": 8.95 },
{ "title": "The Lord of the Rings", "author": "J. R. R. Tolkien", "price": 22.99 }
]
被选中的两个标题都长于 15 个字符;“Moby Dick”(9)和 “Sword of Honour”(15,并非超过 15)都被排除在外。
下面是每个函数在过滤器内部的作用:
length():字符串、数组或对象的长度,如[?length(@.title) > 15]count():节点列表(nodelist)中的节点数,如[?(count(@.authors) > 1)]match():整串正则测试(I-Regexp 模式),如[?match(@.author, 'J.*')]search():子串正则测试,如[?search(@.title, 'the')]value():把单节点的节点列表转换为其值以便比较
这五个都是 RFC 9535 的特性。Classic(Goessner)方言并不实现它们,所以如果一个基于函数的表达式失败了,先确认你是在过滤器内部调用它,并且引擎已设为 RFC 9535。
RFC 9535 与 Classic Goessner——为何同一条表达式结果不同
当一条 JSONPath 表达式在两个工具里返回不同结果时,原因通常就是方言。下面是两者的对比:
| 方面 | Classic Goessner(2007) | RFC 9535(2024) |
|---|---|---|
| 标准化 | 事实标准,从未正式化 | 首个正式的 IETF 规范 |
| filter 空白/引号 | 宽松([?(@.price<10)] 可以) | 严格,完全遵循语法 |
| 缺失成员比较 | 由实现自定义 | 有明确定义,不抛错 |
| 标准函数 | 不属于该方言 | length count match search value |
| 规范化路径 | 无规范形式 | 规范化、单引号方括号形式 |
| 并集顺序 | 因库而异 | 已规定 |
实战建议:如果你的下游系统宣称符合 RFC 9535,就针对标准引擎编写和验证。如果你维护的是从 jsonpath.com、jsonpath-plus 或基于 Jayway 的服务复制来的表达式,就用 Classic,好让结果可复现。JSONPath 测试器用一个开关切换两个引擎,你可以粘贴一次表达式,并排看到每种方言如何处理它。要查清结果为何分歧,这种并排对比往往最直接。
JSONPath vs XPath vs jq——该用哪个
这三者经常被混为一谈,所以这里给个简短版:
- JSONPath 是面向 JSON 的声明式路径查询。它最适合嵌入配置和测试断言,让你不必写代码就能指明一个位置。
- XPath 是 XML 世界的对应物。JSONPath 借用了它的一些记号(
*、..、[]),这正是类比成立的原因,但两种语言不可互换,函数集也不同。 - jq 是命令行 JSON 处理器。它远不止路径选取,还能转换、聚合、重塑,而且就跑在你的 shell 管道里。
选哪个通常很清楚。要做嵌入式断言或流水线配置字段,选 JSONPath。要做 shell 驱动的转换和数据处理,选 jq,jq 速查表深入讲解了这套工作流。而当问题是某个 payload 是否符合预期形状、而非某个字段在哪里时,就用 JSON Schema 校验器及其完整校验指南来验证它。
7 个常见的 JSONPath 错误
- 忘了根
$。store.book会被大多数引擎拒绝;每条表达式都以$开头。 - 切片差一。
[0:2]是两个元素,不是三个,右端边界排除在外。 - 方言搞错。 把 Classic 表达式跑在 RFC 9535 上(或反之)可能解析报错或匹配到不同节点。把引擎切到对应方言。
- 把函数当独立段。
$.store.book.length()不是有效的 RFC 9535;要在过滤器内部调用length()。 - 过滤器里忘了
@。[?($.price < 10)]指向根;要用[?(@.price < 10)]。 - 方括号引号写错。
$[store]是错误;给键加引号:$['store']。 - 以为
..只到第一层。 递归下降在每一层匹配,而非仅直接子节点。
在线测试 JSONPath,且保护隐私
学 JSONPath 语法最快的方式就是运行它。JSONPath 测试器能实时求值本文里的每一条表达式:RFC 9535 与 Classic 双引擎、Values / Paths / Both 三种结果视图、便于调试的规范化路径,以及 100% 浏览器本地执行。它不上传、不注册、不用 eval,所以用于专有 payload 也很安全。在这里搭一条路径,确认它精确选中你想要的节点,再把验证过的表达式直接粘进你的代码、测试或流水线。
至于 JSON 工作流的其余部分,可以用 JSON 转 TypeScript 把一份示例响应变成带类型的接口,或用 JSON Diff 逐字段对比两份文档。
常见问题
JSONPath 用来做什么?
JSONPath 不写命令式代码就能查询 JSON。开发者用它从 API 响应里取字段、在集成测试里对值做断言,以及在 Kubernetes、AWS Step Functions、Azure Logic Apps 的配置里寻址字段。它擅长从庞大或不规则的结构里提取数据,那种情况下手写遍历会很繁琐。
RFC 9535 和经典 JSONPath 有什么区别?
Classic 是 Stefan Goessner 2007 年的事实方言,被广泛实现却从未正式规范化,所以各库出现了分歧。RFC 9535 是 IETF 2024 年 2 月的正式规范:它定义了精确的语法、结果用的规范化路径,以及五个标准函数。两者在过滤器、并集和缺失成员比较等边界处不同。
JSONPath 过滤表达式是怎么工作的?
过滤器 [?()] 只保留谓词为真的元素,@ 是当前元素。例如 $.store.book[?(@.price < 10)] 选出价格低于 10 的书。你可以用 && 和 || 组合条件、测试某个成员是否存在,以及与字符串或数字字面量比较。
我能把 length() 写成 $.store.book.length() 吗?
不能。在 RFC 9535 里,length() 和另外四个函数只能在过滤器内部调用,比如 $.store.book[?length(@.title) > 15]。独立段写法 $.store.book.length() 是 jsonpath-plus 扩展,不是标准 JSONPath,RFC 9535 语法会拒绝它。
JSONPath 和 XPath 是一回事吗?
不是,但思路相似。XPath 查询 XML,JSONPath 查询 JSON,两者都用路径选择器定位节点。JSONPath 刻意借用了 XPath 的部分记号(*、.. 和 []),这让类比很有用,但语法、语义和函数集都不同,且不可互换。
JSONPath 里的递归下降(..)做什么?
.. 运算符搜索文档的每一层,而不只是直接子节点。$..author 会把所有 author 成员收集起来,无论它出现在哪一层嵌套。它是从深层嵌套或不规则结构里提取单个字段的最快方式,但匹配到的节点可能远超预期,能收窄时就收窄。