JsonPath 通关指南

376 阅读2分钟

由于准备基于 JsonPath 自定义一套规则,于是想仔细研究一下 JsonPath,于是整理了一篇通关指南。

github 地址:github.com/json-path/J…

JsonPath 使用符号

JsonPath 可以使用点符号和括号:

点符号 $.store.book[0].title

括号符号: $['store']['book'][0]['title']

素材准备(官网素材):

{
  "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": 19.95
    }
  }
} 

接下来所有的测试都是用这段 JSON代码。 下面所有的测试代码都放在 gitee (gitee.com/uzongn/json…)上面,可以下载测试允许。

操作符号

$

所有路径表达式的开头;也可以理解为 json 的根元素。

// 获取所有书籍的作者
List<String> authors = JsonPath.read(jsonString, "$.store.book[*].author");
assertEquals(4, authors.size());
assertTrue(authors.contains("Nigel Rees"));

注意:所有表达式都是 $开头。

@

表示当前元素,通常在过滤表达式中使用

// 1. 获取所有价格大于10的书籍
List<Map<String, Object>> expensiveBooks = JsonPath.read(jsonString, "$.store.book[?(@.price > 10)]");
assertEquals(2, expensiveBooks.size());

$.store.book[?(@.price > 10)]") 通过 @ 表示当前节点。(类似for循环中的当前变量对象)

*

通配符,表示匹配所有元素。例如,$.store.book[*] 表示访问 store 下的所有 book 元素

..

匹配所有子元素。例如,$.store..price 表示访问所有层级的 price 属性

.

点符号子项

// 获取所有书籍的作者
List<String> authors = JsonPath.read(jsonString, "$.store.book[*].author");
assertEquals(4, authors.size());
assertTrue(authors.contains("Nigel Rees"));

?()

过滤表达式,用于根据条件过滤数组元素。例如,$.store.book[?(@.price < 10)] 表示访问价格小于 10 的所有书籍。

注意:表达式必须计算为布尔值。

// 获取所有fiction类别的书籍
List<Map<String, Object>> fictionBooks = JsonPath.read(jsonString, "$.store.book[?(@.category == 'fiction')]");
assertEquals(3, fictionBooks.size());

[]

用于访问数组的元素。例如,$.store.book[0] 表示访问 store 下的 book 数组的第一个元素。

$.store.book[0,2]: 表示第 1 本和第 3 本

$.store.book[-1]:表示最后一本

[start:end]

对数组进行切片。

// 获取前两本书
List<Map<String, Object>> firstTwoBooks = JsonPath.read(jsonString, "$.store.book[0:2]");
assertEquals(2, firstTwoBooks.size());

注意这里在数学意义上属于左闭右开 [)

$.store.book[0:2]: 表示前两本

$.store.book[-2:]: 表示最后两本

$.store.book[:2]:表示从0到1 (不包含索引2)

&& or || or !

谓词

// 价格范围过滤
List<Map<String, Object>> priceRange = JsonPath.read(jsonString,
    "$.store.book[?(@.price >= 8 && @.price <= 12)]");
assertEquals(2, priceRange.size());

使用&&||来组合多个谓词[?(@.price < 10 && @.category == 'fiction')][?(@.category == 'reference' || @.price > 10)]

用来!否定谓词[?(!(@.price < 10 && @.category == 'fiction'))]

操作符

=~

正则匹配。 从书籍数组中筛选出标题包含 "of" 的书籍,并提取其标题

List<String> matchingTitles = JsonPath.read(jsonString,
    "$.store.book[?(@.title =~ /.*of.*/i)].title");
assertEquals(2, matchingTitles.size());

i 是一个修饰符(或标志),表示不区分大小写(case-insensitive)

函数

distinct

// 获取不重复的分类
List<String> uniqueCategories = JsonPath.read(jsonString,
    "$.store.book[*].category.distinct()");
assertTrue(uniqueCategories.size() < 4); // 因为有重复的 fiction 类别

sort

// 对价格进行排序
List<Double> sortedPrices = JsonPath.read(jsonString,
    "$.store.book[*].price.sort()");
assertTrue(sortedPrices.get(0) < sortedPrices.get(sortedPrices.size() - 1));

reverse

// 反向排序
List<Double> reverseSortedPrices = JsonPath.read(jsonString,
    "$.store.book[*].price.sort().reverse()");
assertTrue(reverseSortedPrices.get(0) > reverseSortedPrices.get(reverseSortedPrices.size() - 1));

toUpperCase/replace

    // 转换为大写
    List<String> upperTitles = JsonPath.read(jsonString,
        "$.store.book[*].title.toUpperCase()");
    assertTrue(upperTitles.stream().allMatch(t -> t.equals(t.toUpperCase())));
    
    // 字符串替换
    List<String> modifiedTitles = JsonPath.read(jsonString,
        "$.store.book[*].title.replace(' ', '_')");
    assertTrue(modifiedTitles.stream().anyMatch(t -> t.contains("_")));

round/ceil/floor

    // 四舍五入
    List<Double> roundedPrices = JsonPath.read(jsonString,
        "$.store.book[*].price.round()");
    assertTrue(roundedPrices.stream().allMatch(p -> p == Math.round(p)));
    
    // 向上取整
    List<Double> ceilingPrices = JsonPath.read(jsonString,
        "$.store.book[*].price.ceil()");
    assertTrue(ceilingPrices.stream().allMatch(p -> p >= 0));
    
    // 向下取整
    List<Double> floorPrices = JsonPath.read(jsonString,
        "$.store.book[*].price.floor()");
    assertTrue(floorPrices.stream().allMatch(p -> p >= 0));

contains/startsWith/endsWith

// 使用 contains
List<Map<String, Object>> booksWithOf = JsonPath.read(jsonString,
    "$.store.book[?(@.title.contains('of'))]");
assertTrue(booksWithOf.size() > 0);

// 使用 startsWith
List<Map<String, Object>> booksStartWithThe = JsonPath.read(jsonString,
    "$.store.book[?(@.title.startsWith('The'))]");
assertTrue(booksStartWithThe.size() > 0);

// 使用 endsWith
List<Map<String, Object>> booksEndWithS = JsonPath.read(jsonString,
    "$.store.book[?(@.title.endsWith('s'))]");
assertTrue(booksEndWithS.size() > 0);

常见陷阱与解决方案

空值处理

# 可能导致NPE的写法
$.store.book[?(@.price > 10)]

# 安全的写法
$.store.book[?(@.price != null && @.price > 10)]

类型转换问题

# 可能的类型转换问题
$.store.book[?(@.price == "10.99")]

# 正确的写法
$.store.book[?(@.price == 10.99)]

避免递归下降

# 不推荐
$..book[*].price   # 性能较差,可能遍历整个文档

# 推荐
$.store.book[*].price   # 明确的路径,性能更好

数组边界检查

# 不安全
$.store.book[0].title

# 安全
$.store.book[?(@.length() > 0)][0].title

# 或使用默认值(某些实现支持)
$.store.book[0].title default 'Unknown'

其他

性能相关

JSONPath 表达式的效率取决于其复杂度和数据量。在处理大型 JSON 数据时,过于复杂的表达式会显著影响性能。例如,尽量避免使用过多的递归查询或复杂的过滤条件。

最后

对于 JsonPath 的使用,就到这里,基本可以应付80%的场景,如果需要可以查阅官网了解更多细节。github.com/json-path/J…