首页 > 基础资料 博客日记
RFC 9535:JSONPath 的标准化之路
2026-04-09 11:00:02基础资料围观1次
从 Stefan Gössner 2007 年的博客文章,到 2024 年正式成为 IETF 标准,JSONPath 走过了 17 年的标准化历程。本文带你深入了解 RFC 9535 的核心特性,并用 snack4-jsonpath 实战演示。
1. 为什么需要 JSONPath?
在 JSON 统治 API 世界的今天,我们几乎每天都在处理 JSON 数据。你是否遇到过这样的场景:
- 从复杂的嵌套 JSON 中提取特定字段
- 在多层嵌套的数组中筛选符合条件的元素
- 对 API 返回的 JSON 进行灵活的数据转换
传统的方案要么需要编写大量代码遍历解析,要么依赖不兼容的各种实现。RFC 9535 的出现,终于结束了这种混乱局面。
2. RFC 9535 是什么?
RFC 9535 是 IETF(互联网工程任务组)于 2024 年 2 月正式发布的标准规范,全称:
JSONPath: Query Expressions for JSON
即「用于 JSON 的查询表达式」
该规范由三位作者共同编写:
- Stefan Gössner — JSONPath 的创始人,早在 2007 年就提出了这一概念
- Glyn Normington — RFC 编辑
- Carsten Bormann — RFC 编辑
核心定义
RFC 9535 的核心可以概括为:
JSONPath 定义了一种字符串语法,用于从给定的 JSON 值中选择和提取 JSON 值。
简单来说,JSONPath 就是 JSON 的「XPath」——用类似路径表达式的方式查询 JSON 数据。
3. JSONPath vs 其他方案
| 特性 | JSONPath | JMESPath | JSON Pointer |
|---|---|---|---|
| 标准化 | RFC 9535 (2024) | AWS 标准 | RFC 6901 |
| 语法风格 | 类似 XPath | 函数式 | 路径式 |
| 递归下降 | ✅ .. |
✅ ** |
❌ |
| 过滤能力 | ✅ 强大 | ✅ 强大 | ❌ 简单查找 |
| 数组切片 | ✅ | ❌ | ❌ |
| 函数扩展 | ✅ | ✅ | ❌ |
4. 核心语法一览
4.1 基本选择器
import org.noear.snack4.jsonpath.JsonPath;
String json = "{\"store\":{\"book\":[{\"author\":\"张三\",\"price\":8.95},{\"author\":\"李四\",\"price\":12.99}],\"bicycle\":{\"color\":\"red\",\"price\":399}}}";
// 根节点选择
JsonPath.select(json, "$"); // 整个文档
// 点号记法
JsonPath.select(json, "$.store.book"); // 选取 store.book 数组
JsonPath.select(json, "$.store.bicycle.color"); // "red"
// 括号记法
JsonPath.select(json, "$['store']['book']"); // 同上
4.2 通配符选择器
import org.noear.snack4.jsonpath.JsonPath;
// 选择所有子节点
JsonPath.select(json, "$.store.*"); // [数组, 对象] - store 下的所有成员
// 选择对象或数组的所有元素
JsonPath.select(json, "$.store.book[*]"); // 所有书籍
JsonPath.select(json, "$.store.book[*].author"); // ["张三", "李四"]
4.3 索引与数组切片
import org.noear.snack4.jsonpath.JsonPath;
// 索引选择(从 0 开始)
JsonPath.select(json, "$.store.book[0]"); // 第一本书
JsonPath.select(json, "$.store.book[-1]"); // 最后一本书
// 数组切片 [start:end:step]
JsonPath.select(json, "$.store.book[0:2]"); // 前两本书
JsonPath.select(json, "$.store.book[::2]"); // 隔一本取一本
JsonPath.select(json, "$.store.book[1:]"); // 从第二本开始的所有书
4.4 递归下降 ..
这是 JSONPath 最强大的特性之一:
// 递归查找所有 author 字段
JsonPath.select(json, "$..author"); // ["张三", "李四"]
// 递归查找所有 price 字段
JsonPath.select(json, "$..price"); // [8.95, 12.99, 399]
// 递归查找所有包含 author 的节点
JsonPath.select(json, "$..book[?(@.author)]");
4.5 过滤表达式 [?(...)]
RFC 9535 的过滤表达式使用 @ 代表当前节点:
// 基础比较
JsonPath.select(json, "$.store.book[?(@.price < 10)]");
// 结果:[{"author":"张三","price":8.95}]
// 字符串匹配
JsonPath.select(json, "$.store.book[?(@.author == '张三')]");
// 复合条件
JsonPath.select(json, "$.store.book[?(@.price > 10 && @.price < 20)]");
// 结果:[{"author":"李四","price":12.99}]
// 检查属性存在性
JsonPath.select(json, "$.store.book[?(@.isbn)]"); // 有 isbn 字段的书
5. 函数扩展
RFC 9535 定义了标准函数扩展接口,snack4-jsonpath 完整实现:
5.1 内置函数
// length() - 获取长度
JsonPath.select(json, "length($.store.book)"); // 2
// count() - 计数(RFC 9535)
JsonPath.select(json, "count($.store.book)"); // 2
// keys() - 获取对象的所有键
JsonPath.select(json, "keys($.store.bicycle)"); // ["color", "price"]
// 配合过滤使用
JsonPath.select(json, "$.store.book[?count(@) > 0]"); // 非空书籍
5.2 字符串函数
// match() - 正则匹配(需启用完整模式)
JsonPath.select(json, "$.store.book[?match(@.author, '张.*')]");
// search() - 搜索(包含)
JsonPath.select(json, "$.store.book[?search(@.author, '三')]");
// value() - 获取值或默认值
// JsonPath.select(json, "value($.store.book[0].price, 0)"); // 8.95
5.3 扩展聚合函数(Jayway 风格)
// min() / max() / avg() / sum()
String enhancedJson = "{\"prices\":[8.95,12.99]}";
JsonPath.select(enhancedJson, "$.prices.min()"); // 8.95
JsonPath.select(enhancedJson, "$.prices.max()"); // 12.99
JsonPath.select(enhancedJson, "$.prices.avg()"); // 10.97
6. 操作符详解
6.1 RFC 9535 标准操作符
// 比较操作符
@.price == 10 // 等于
@.price != 10 // 不等于
@.price > 10 // 大于
@.price >= 10 // 大于等于
@.price < 10 // 小于
@.price <= 10 // 小于等于
// 逻辑操作符
@.price > 10 && @.price < 20 // AND
@.author == '张三' || @.author == '李四' // OR
!(@.price > 10) // NOT
6.2 扩展操作符(Jayway 风格)
// 正则匹配
@.author =~ /张.*/
// 集合操作
@.status in ["active", "pending"]
@.age nin [10, 20] // not in
@.role anyof ["admin", "user"] // 任一匹配
@.tags subsetof ["a","b","c"] // 子集关系
// 字符串操作
startsWith(@.name, '张')
endsWith(@.email, '@example.com')
contains(@.tags, 'vip')
// 值检查
empty(@.children) // 是否为空
size(@.items) == 5 // 集合大小
7. 实际应用场景
7.1 API 响应解析
String apiResponse = """
{
"code": 200,
"data": {
"users": [
{"id": 1, "name": "Alice", "orders": [{"amount": 100}, {"amount": 200}]},
{"id": 2, "name": "Bob", "orders": [{"amount": 150}]},
{"id": 3, "name": "Charlie", "orders": []}
]
}
}
""";
// 提取所有用户名
JsonPath.select(apiResponse, "$.data.users[*].name");
// ["Alice", "Bob", "Charlie"]
// 找出有订单的用户
JsonPath.select(apiResponse, "$.data.users[?(@.orders && length(@.orders) > 0)].name");
// ["Alice", "Bob"]
// 计算每个用户的订单总额
JsonPath.select(apiResponse, "$.data.users[*].orders[*].amount");
// [100, 200, 150]
7.2 配置管理
String config = """
{
"environments": {
"dev": {"host": "localhost", "port": 8080},
"staging": {"host": "staging.example.com", "port": 80},
"prod": {"host": "prod.example.com", "port": 443, "ssl": true}
},
"current": "prod"
}
""";
// 动态获取当前环境的配置
String currentEnv = JsonPath.select(config, "$.current").asString();
String host = JsonPath.select(config, "$.environments." + currentEnv + ".host").asString();
// "prod.example.com"
7.3 数据校验与转换
// 提取并验证数据
String json = """
{
"products": [
{"name": "笔记本", "price": 4999, "stock": 100},
{"name": "鼠标", "price": 99, "stock": 0}
]
}
""";
// 找出缺货商品
JsonPath.select(json, "$.products[?(@.stock == 0)].name");
// ["鼠标"]
// 找出价格超过 1000 的商品
JsonPath.select(json, "$.products[?(@.price > 1000)].name");
// ["笔记本"]
8. snack4-jsonpath 的双模式支持
snack4-jsonpath 同时支持 RFC 9535(IETF)模式和 Jayway 模式:
8.1 模式差异
| 特性 | RFC 9535 (默认) | Jayway 模式 |
|---|---|---|
| 过滤行为 | 仅过滤子节点 | 递归过滤当前及子节点 |
.. 行为 |
RFC 标准语义 | 扩展语义 |
| 扩展操作符 | 支持(但不属于规范) | ✅ 支持 |
| 扩展函数 | 支持(但不属于规范) | ✅ 支持 |
8.2 模式切换
import org.noear.snack4.Options;
import org.noear.snack4.Feature;
// RFC 9535 模式(默认)
JsonPath jp1 = JsonPath.parse("$.store.book[?(@.price > 10)]");
// Jayway 兼容模式
Options jaywayOpts = new Options(Feature.JsonPath_JaywayMode);
// 需要通过自定义方式应用选项...
9. 语法速查表
| 语法 | 说明 | 示例 |
|---|---|---|
$ |
根节点 | $ |
@ |
当前节点(过滤中) | [?(@.price > 10)] |
.key |
子属性 | $.store.book |
['key'] |
括号记法 | $['store']['book'] |
* |
通配符 | $.store.* |
[0] |
索引 | $.book[0] |
[-1] |
末尾索引 | $.book[-1] |
[start:end] |
切片 | $.book[0:2] |
[::step] |
步长 | $.book[::2] |
..key |
递归下降 | $..author |
[?()] |
过滤表达式 | [?(@.price < 10)] |
, |
多选 | ['a','b'] |
length() |
长度函数 | length($.items) |
count() |
计数函数 | count($.items) |
10. 与 XPath 的渊源
RFC 9535 附录 B 专门讨论了 JSONPath 与 XPath 的关系。
JSONPath 从 XPath 汲取了大量灵感:
| XPath | JSONPath | 含义 |
|---|---|---|
/ |
$ |
文档根 |
./ |
@ |
当前节点 |
* |
* |
通配符 |
// |
.. |
递归下降 |
[@attr='v'] |
[?(@.attr=='v')] |
过滤条件 |
path/a/b |
path.a.b |
子路径 |
但 JSONPath 有自己的特色:
- 更简洁的语法
- 原生支持数组索引和切片
- 针对 JSON 结构优化的查询语义
11. 标准化带来的好处
11.1 跨平台一致性
以前:同一段 JSONPath 表达式在不同库中可能有不同的行为。
现在:遵循 RFC 9535 的实现必须产生一致的输出。
11.2 正式测试套件
RFC 9535 配套了官方的 JSONPath Compliance Test Suite (CTS),实现者可以用它验证规范符合度。
11.3 安全考虑
RFC 9535 第 4 节专门讨论了安全问题:
- 查询注入:恶意构造的查询可能耗尽资源
- 路径遍历:类似文件系统的
..攻击 - 正则表达式 DoS:复杂正则可能导致 ReDoS
snack4-jsonpath 通过以下方式应对:
// 可选的异常抑制
Options opts = new Options(Feature.JsonPath_SuppressExceptions);
// 查询失败时返回空结果而非抛出异常
12. 进阶技巧
12.1 链式查询
String json = """
{
"users": [
{"name": "Alice", "age": 30, "city": "Beijing"},
{"name": "Bob", "age": 25, "city": "Shanghai"}
]
}
""";
// 找出年龄最大的用户所在城市
String maxAgeCity = JsonPath.select(json,
"$.users[?(@.age == max($..age))].city"
).asString();
// "Beijing"
12.2 路径归一化
// 获取归一化路径(Normalized Path)
String path = JsonPath.select(json, "$.users[0].name").getPath();
// "$['users'][0]['name']"
12.3 动态路径构建
// 解析后缓存,可重复使用
JsonPath path = JsonPath.parse("$.store.$.category[*]");
// 多次查询复用
for (String category : categories) {
JsonPath compiledPath = JsonPath.parse("$.store." + category + "[*]");
// 使用 compiledPath 查询
}
结语
RFC 9535 的发布标志着 JSONPath 进入了一个新的时代。从 2007 年的博客文章到 2024 年的 IETF 标准,这条路走了整整 17 年。
标准化的价值在于:
- 开发者可以编写一次,到处运行
- 工具厂商有了统一的规范遵循
- 新实现有了明确的参考
snack4-jsonpath 作为 RFC 9535 的 Java 实现,不仅完整支持了标准规范,还通过 Jayway 兼容模式保留了扩展功能。无论你是需要标准兼容性还是扩展能力,都能找到合适的方案。
相关资源:
- RFC 9535 原文:https://www.rfc-editor.org/rfc/rfc9535
- snack-jsonpath 项目:https://github.com/noear/snack-jsonpath
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签:
相关文章
最新发布
- 推荐3个牛逼的 AI Agent系统(要涨了)!
- AI开发-python-langchain框架(3-21-Structured Chat ReAct 智能体构建及对比 ZeroShot ReAct)
- 从零学习Kafka:位移与高水位
- 为什么给 new 设计一个 realloc 是必要的
- RFC 9535:JSONPath 的标准化之路
- 拆解 Claude Code SubAgent:隔离、专业化与权限设计
- 【从0到1构建一个ClaudeAgent】工具与执行-Agent循环
- ClawHub 24 小时热门 Top 10 | 2026 年 04 月 09 日
- 在 Web 界面直接编辑 DESIGN.md:从思路到实现
- 电子小白之集成电路

