全球首个支持 IETF JSONPath (RFC 9535) 标准的 Java 框架,Snack4-Jsonpath v4.0.0 发布

基于jdk8。支持:Json Dom 的构建、编码解转换、获取、JsonPath 查询、JsonSchema 验证。

xml 复制代码
<dependency>
  <groupId>org.noear</groupId>
  <artifactId>snack4-jsonpath</artifactId>
  <version>4.0.0</version>
</dependency>

Snack-Jsonpath 借鉴了 Javascript 所有变量由 var 申明,及 Xml dom 一切都是 Node 的设计。其下一切数据都以ONode表示,ONode也即 One node 之意,代表任何类型,也可以转换为任何类型。

  • 强调文档树的构建和操控能力
  • 高性能Json path查询(比 jayway.jsonpath 快很多倍)。同时兼容 jayway.jsonpath 和 IETF JSONPath (RFC 9535) 标准 (用 options 切换)。为下一个十年提供强劲的 JsonPath 体验。
  • 支持 Json schema 架构校验
  • 支持 json5 部分特性(无键字段,注释,等...)
  • 优先使用 无参构造函数 + 字段 编解码(可减少注入而触发动作的风险)

依赖包清单:

依赖包 描述
org.noear:snack4 提供 json dom 构建和编解码支持
org.noear:snack4-jsonpath 提供 json path 查询支持
org.noear:snack4-jsonschema 提供 json schema 校验支持

开源项目仓库地址:

文档资料:

1、版本更新说明

  • 重构整个项目(除了名字没变,其它都变了)
  • 单测覆盖率 98%,历时小半年
  • 支持 IETF JSONPath (RFC 9535) 标准(全球首个支持该标准的 Java 框架)。同时兼容 jayway.jsonpath
  • 添加 json-schema 支持

2、JSONPath 语法参考

语法元素 描述
$ 根节点标识符
@ 当前节点标识符(仅在过滤选择器中有效)
[<selectors>] 子段:选择节点的零个或多个子节点
.name 简写 ['name']
.* 简写 [*]
..[<selectors>] 后代段:选择节点的零个或多个后代
..name 简写 ..['name']
..* 简写 ..[*]
'name' 名称选择器:选择对象的命名子对象
* 通配符选择器:选择节点的所有子节点
3 索引选择器:选择数组的索引子项(从 0 开始)
0:100:5 数组切片选择器:数组的 start:end:step
?<logical-expr> 过滤选择器:使用逻辑表达式选择特定的子项
fun(@.foo) 过滤函数:在过滤表达式中调用函数(IETF 标准)
.fun() 聚合函数:作为片段使用(jayway 风格)

过滤选择器语法参考:

语法 描述 优先级
(...) 分组 5
name(...) 函数扩展 5
! 逻辑 非 4
==,!=,<,<=,>,>= 关系比较符 3
&& 逻辑 与 2
` `

IETF JSONPath (RFC 9535) 标准定义操作符(支持)

操作符 描述 示例
== 左等于右(注意1不等于'1') $[?(@.a == 1)]
!= 左不等于右 $[?(@.a != 1)]
< 左比右小 $[?(@.a < 1)]
<= 左小于或等于右 $[?(@.a <= 1)]
> 左大于右 $[?(@.a > 1)]
>= 左大于等于右 $[?(@.a >= 1)]

jayway.jsonpath 增量操作符(支持)

操作符 描述 示例
=~ 左匹配正则表达式 [?(@.s =~ /foo.*?/i)]
in 左存在于右 [?(@.s in ['S', 'M'])]
nin 左不存在于右
subsetof 左是右的子集 [?(@.s subsetof ['S', 'M', 'L'])]
anyof 左与右有一个交点 [?(@.s anyof ['M', 'L'])]
noneof 左与右没有交集 [?(@.s noneof ['M', 'L'])]
size 左(数组或字符串)的大小应该与右匹配 $[?(@.s size @.expected_size)]
empty Left(数组或字符串)应该为空 $[?(@.s empty false)]

IETF JSONPath (RFC 9535) 标准定义函数(支持)

函数 描述 参数类型 结果类型
length(x) 字符串、数组或对象的长度 值 数值
count(x) 节点列表的大小 节点列表 数值
match(x,y) 正则表达式完全匹配 值,值 逻辑值
search(x,y) 正则表达式子字符串匹配 值,值 逻辑值
value(x) 节点列表中单个节点的值 节点列表 值

jayway.jsonpath 函数(支持)

函数 描述 输出类型
length() 字符串、数组或对象的长度 Integer
min() 查找当前数值数组中的最小值 Double
max() 查找当前数值数组中的最大值 Double
avg() 计算当前数值数组中的平均值 Double
stddev() 计算当前数值数组中的标准差 Double
sum() 计算当前数值数组中的总和 Double
keys() 计算当前对象的属性键集合 Set<E>
concat(X) 将一个项或集合和当前数组连接成一个新数组 like input
append(X) 将一个项或集合 追加到当前路径的输出数组中 like input
first() 返回当前数组的第一个元素 依赖于数组元素类型
last() 返回当前数组的最后一个元素 依赖于数组元素类型
index(X) 返回当前数组中索引为X的元素。X可以是负数(从末尾开始计算) 依赖于数组元素类型

snack-jsonpath 增量操作符(支持)

操作符 描述 示例
startsWith 左(字符串)开头匹配右 [?(@.s startsWith 'a')]
endsWith 左(字符串)结尾匹配右 [?(@.s endsWith 'b')]
contains 左(数组或字符串)包含匹配右 [?(@.s contains 'c')]

3、JSONPath 语法示例

JSON 样本数据

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": 399
    }
  }
}

示例JSONPath表达式及其应用于示例JSON值时的预期结果

JSONPath 预期结果
$.store.book[*].author 书店里所有书的作者
$..autho 所有作者
$.store.* 商店里的所有东西,包括一些书和一辆红色的自行车
$.store..price 商店里所有东西的价格
$..book[2] 第三本书
$..book[2].author 第三本书的作者
$..book[2].publisher 空结果:第三本书没有"publisher"成员
$..book[-1] 最后一本书
$..book[0,1] $..book[:2] 前两本书
$..book[?@.isbn] 所有有国际标准书号的书
$..book[?@.price<10] 所有比10便宜的书
$..* 输入值中包含的所有成员值和数组元素

4、放几个应用示例看看

支持 dom 操控

java 复制代码
ONode oNode = new ONode();
oNode.set("id", 1);
oNode.getOrNew("layout").then(o -> {
    o.addNew().set("title", "开始").set("type", "start");
    o.addNew().set("title", "结束").set("type", "end");
});

oNode.get("id").getInt();
oNode.get("layout").get(0).get("title").getString();

oNode.getOrNew("list").fillJson("[1,2,3,4,5,6]");

支持 json path 查询、构建、删除

java 复制代码
ONode.ofBean(store).select("$..book[?@.tags contains 'war'].first()").toBean(Book.class); //RFC9535 规范,可以没有括号
ONode.ofBean(store).select("$..book[?(!(@.category == 'fiction') && @.price < 40)].first()").toBean(Book.class);
ONode.ofJson(store).select("$.store.book.count()");

ONode.ofBean(store).create("$.store.book[0].category").toJson();

ONode.ofBean(store).delete("$..book[-1]");

支持 json schema 校验

java 复制代码
JsonSchema schema = JsonSchema.ofJson("{type:'object',properties:{userId:{type:'string'}}}"); //加载架构定义

schema.validate(ONode.load("{userId:'1'}")); //校验格式

支持序列化、反序列化

java 复制代码
User user = new User();
ONode.ofBean(user).toBean(User.class); //可以作为 bean 转换使用
ONode.ofBean(user).toJson();

ONode.ofJson("{}").toBean(User.class);
ONode.ofJson("[{},{}]").toBean((new ArrayList<User>(){}).getClass());

//快捷方式
String json = ONode.serialize(user);
User user = ONode.deserialize(json, User.class);

5、路径树接口

java 复制代码
//case1
ONode o = ONode.ofJson(json);
ONode rst = o.select("$.data.list[*].mobile"); //自动为查询到的节点,生成 path 属性
List<String> rstPaths = rst.pathList(); //获取结果节点的路径列表
for(ONode n1 : rst.getArray()) {
    n1.path(); //当前路径
    n1.parent(); //父级节点
}

//case2
ONode o = ONode.ofJson(json).usePaths(); //手动为每个子节点,生成 path 属性
ONode rst = o.get("data").get("list").get(2);
rst.path();
rst.parent();

6、高级定制

Json 编解码定制

java 复制代码
Options options = Options.of();
//添加编码器
options.addEncoder(Date.class, (ctx, value, target) -> {
    target.setValue(DateUtil.format(data, "yyyy-MM-dd"));
});
//添加解码器
options.addDecoder(Date.class, ...);
//添加创建器(接管类实例化)
options.addCreator(...);

//添加特性
options.addFeature(Feature.Write_PrettyFormat);

//移除特性
options.removeFeature(Feature.Write_PrettyFormat);

//设置日期格式附
options.addFeature(Feature.Write_UseDateFormat); //使用日期格式
options.dateFormat("yyyy-MM");

//..

String json = ONode.ofBean(orderModel, options).toJson();

JsonPath 函数与操作符定制

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

public class FunctionDemo {
    public static void main(String[] args) {
        //定制 floor 函数
        FunctionLib.register("floor", (ctx, argNodes) -> {
            ONode arg0 = argNodes.get(0); //节点列表(选择器的结果)

            if (ctx.isDescendant()) {
                for (ONode n1 : arg0.getArray()) {
                    if (n1.isNumber()) {
                        n1.setValue(Math.floor(n1.getDouble()));
                    }
                }

                return arg0;
            } else {
                ONode n1 = arg0.get(0);

                if (n1.isNumber()) {
                    return ctx.newNode(Math.floor(n1.getDouble()));
                } else {
                    return ctx.newNode();
                }
            }
        });

        //检验效果(在 IETF 规范里以子项进行过滤,即 1,2) //out: 1.0
        System.out.println(ONode.ofJson("{'a':1,'b':2}")
                .select("$.a.floor()")
                .toJson());

        //参考 //out: 2.0
        System.out.println(ONode.ofJson("{'a':1,'b':2}")
                .select("$[?floor(@) > 1].first()")
                .toJson());
    }
}
java 复制代码
import org.noear.snack4.ONode;
import org.noear.snack4.jsonpath.OperatorLib;

public class OperationDemo {
    public static void main(String[] args) {
        //定制操作符
        OperatorLib.register("startsWith", (ctx, node, term) -> {
            ONode leftNode = term.getLeftNode(ctx, node);

            if (leftNode.isString()) {
                ONode rightNode = term.getRightNode(ctx, node);
                if (rightNode.isNull()) {
                    return false;
                }

                return leftNode.getString().startsWith(rightNode.getString());
            }
            return false;
        });

        //检验效果
        assert ONode.ofJson("{'list':['a','b','c']}")
                .select("$.list[?@ startsWith 'a']")
                .size() == 1;
    }
}
相关推荐
凉茶钱3 分钟前
【吃透C++】万字解析类和对象
开发语言·c++
白杨尚青26 分钟前
C++入门篇(十):string(上)——认识string:构造与三大遍历(一条龙讲透operator[]、迭代器、auto、范围for)
java·开发语言·c++·笔记·stl
可乐鸡翅yeah_31 分钟前
hls.js 切换多个视频源,新手开发常见踩坑
开发语言·前端·javascript·ios·ffmpeg·音视频·safari
Tanshu_API君34 分钟前
API 聚合平台选型指南:从参数拆解到生产落地的十个评估维度
开发语言·api
Chen—LSN35 分钟前
C语言——⽂件操作(1)
c语言·开发语言·c++·经验分享·笔记·算法·链表
狗凯之家源码网38 分钟前
PHP 开源 IM 即时通讯系统效果实测与功能展示
开发语言·开源·php
anew___39 分钟前
《从零手写操作系统 (11):文件系统初探——initrd与VFS抽象层》
java·javascript·算法
天天向上29142 分钟前
Spring AI 实战:14 个踩坑记录,其中一个让我的测试全绿但产品是坏的
java·开源
正在走向自律1 小时前
从只写接口到独立交付前后端,飞算JavaAI能给Java后端工程师多少溢价?
java·springboot·ai编程·全栈开发·java求职·飞算javaai·金九银十
辛苦才能1 小时前
C++ map/set 深度解析:从关联式容器的本质到 operator[] 的三重身份
开发语言·c++