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

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 兼容模式保留了扩展功能。无论你是需要标准兼容性还是扩展能力,都能找到合适的方案。

相关资源

posted @ 2026-04-09 10:48  带刺的坐椅  阅读(136)  评论(0)    收藏  举报