RFC 9535:JSONPath 的标准化之路

从 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 基本选择器

java 复制代码
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 通配符选择器

java 复制代码
import org.noear.snack4.jsonpath.JsonPath;

// 选择所有子节点
JsonPath.select(json, "$.store.*");     // [数组, 对象] - store 下的所有成员

// 选择对象或数组的所有元素
JsonPath.select(json, "$.store.book[*]");  // 所有书籍
JsonPath.select(json, "$.store.book[*].author");  // ["张三", "李四"]

4.3 索引与数组切片

java 复制代码
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 最强大的特性之一:

java 复制代码
// 递归查找所有 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 的过滤表达式使用 @ 代表当前节点:

java 复制代码
// 基础比较
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 内置函数

java 复制代码
// 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 字符串函数

java 复制代码
// 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 风格)

java 复制代码
// 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 标准操作符

java 复制代码
// 比较操作符
@.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 风格)

java 复制代码
// 正则匹配
@.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 响应解析

java 复制代码
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 配置管理

java 复制代码
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 数据校验与转换

java 复制代码
// 提取并验证数据
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 模式切换

java 复制代码
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 通过以下方式应对:

java 复制代码
// 可选的异常抑制
Options opts = new Options(Feature.JsonPath_SuppressExceptions);
// 查询失败时返回空结果而非抛出异常

12. 进阶技巧

12.1 链式查询

java 复制代码
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 路径归一化

java 复制代码
// 获取归一化路径(Normalized Path)
String path = JsonPath.select(json, "$.users[0].name").getPath();
// "$['users'][0]['name']"

12.3 动态路径构建

java 复制代码
// 解析后缓存,可重复使用
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 兼容模式保留了扩展功能。无论你是需要标准兼容性还是扩展能力,都能找到合适的方案。

相关资源

相关推荐
神の愛2 小时前
java日志功能
java·开发语言·前端
却话巴山夜雨时i2 小时前
互联网大厂Java面试:从Spring到微服务的全栈挑战
java·spring boot·redis·微服务·面试·kafka·技术栈
ch.ju2 小时前
Java程序设计(第3版)第二章——java的数据类型:字符 char
java
尘世壹俗人2 小时前
idea提交git版本由于中文文件名卡死不动
java·git·intellij-idea
深挖派2 小时前
IntelliJ IDEA 2026.1 安装配置与高效开发环境搭建 (保姆级图文教程)
java·ide·intellij-idea
起个名特麻烦2 小时前
SpringBoot全局配置LocalDate/LocalTime/LocalDateTime的序列化和反序列化
java·spring boot·后端
高斯林.神犇2 小时前
四、依赖注入.spring
java·后端·spring
hero.fei2 小时前
在springboot中使用Resilience4j
java·spring boot·后端
沐苏瑶2 小时前
Java算法之排序
java·算法·排序算法