它解决的是取值麻烦

调一个接口,返回体里几十个字段,真正要用的可能只有其中三个。用代码一层层往下点,写着写着就成了一长串 response.data.items[0].detail.price,字段再嵌套几层,读起来很累。

JSONPath 把这串访问路径变成一个字符串,交给库去解析。这样路径可以放在配置文件里,也可以让运营在后台填,不用改代码。做接口测试、配数据映射、写采集规则时经常见到它。

它最早是 2007 年有人在博客上提出的一个想法,后来被各种语言的库分别实现。所以它不像 JSON 本身那样有正式标准,不同库的细节会有出入,这一点后面会展开。

先用一段数据把符号认全

拿一个书店的返回体举例,结构不算复杂,但足够覆盖常见写法。

{
  "store": {
    "name": "城西书店",
    "books": [
      { "title": "长夜", "price": 39, "tags": ["小说"] },
      { "title": "工具论", "price": 88, "tags": ["技术", "随笔"] },
      { "title": "旧地图", "price": 25, "tags": [] }
    ]
  }
}

$ 表示根节点,所有路径都从它开始。接着用点号取字段,用方括号取数组下标。

写法取到什么
$.store.name城西书店
$.store.books[0].title长夜
$.store.books[*].title三本书的标题组成的数组
$.store.books[-1].title旧地图,负数从末尾往前数
$..price所有层级里叫 price 的值

第三行里的星号是通配符,表示这个数组的每个元素都取一遍。要注意结果的形态:带星号的路径通常返回数组,哪怕只匹配到一个元素,拿到的也是长度为 1 的数组,不是单个值。写代码时按数组处理,别直接当字符串用。

方括号里还可以写切片,语法和 Python 的列表切片很像。$.store.books[0:2] 取前两本,$.store.books[::2] 隔一个取一个。切片在分页展示或者只取前几条时很方便。

键名不规矩的时候

接口字段不一定都是干净的英文单词。带空格、带点、带横线的情况很常见,比如 user name、a.b、content-type。这时候点号写法会出错,因为点被当成了层级分隔符。

换成方括号加引号就行:$['user name']、$['a.b']、$['content-type']。中文键名同样处理。这个写法在大部分库里都支持,记不住的时候直接用方括号,兼容性最好。

过滤表达式

数组里只想挑满足条件的元素,就要用过滤。写法是 [?(条件)],其中 @ 代表当前正在判断的元素。

$.store.books[?(@.price < 50)].title
// 结果:["长夜", "旧地图"]

$.store.books[?(@.tags.length > 0)].title
// 结果:["长夜", "工具论"]

比较运算符和常见语言差不多:==、!=、<、<=、>、>=。多个条件可以用 && 和 || 连起来,比如 [?(@.price < 50 && @.tags.length > 0)]。有些库还支持正则匹配,写法是 =~ /pattern/,但这条不是所有实现都有,换库前先确认。

字符串值必须加引号,[?(@.status == 'ok')] 才对。不加引号的话,解析器会把它当成字段名去找,结果通常是空数组,也不会报错,很容易查半天。数字和布尔值不用引号,但要注意类型,字符串 "100" 和数字 100 在比较时并不相等。

递归下降很方便,也别乱用

.. 表示从当前节点往下找所有层级。数据结构不确定,或者只记得字段名不记得层级时,它很省事。比如日志里每个事件都可能在不同深度带一个 traceId,用 $..traceId 一把就能捞出来。

代价是它会遍历整棵树。几百 KB 的响应没什么感觉,几 MB 的数据、嵌套很深的对象,耗时就会上来。线上做实时处理时,能写确定路径就别用递归下降。真要用,也尽量缩小范围,先定位到某个子树再递归,而不是从根节点开始扫全量。

几个容易踩的细节

  • 路径不存在时,有的库返回空数组,有的返回 null,有的直接抛异常。跨库迁移时先测这一条,别假设行为一致。
  • 过滤表达式里的引号在 JSON 字符串里要转义。把路径写进配置文件时,单引号和双引号的选择会直接影响能不能解析。
  • 部分库支持在过滤条件里用 $ 引用根节点,比如比较两个字段的大小。这个扩展也不是通用的。
  • 数组下标越界通常不报错,只是拿不到值。排查时先确认数据长度,再怀疑路径写错。
  • 同一段路径在不同语言的库上结果可能不同,尤其是涉及排序、去重和函数调用的写法。选库之后用它自己的文档,不要照搬另一门语言的示例。

和 SQL、XPath 的关系

SQL 面向表,XPath 面向 XML 节点,JSONPath 面向 JSON 的值。三者解决的问题相似,都是描述“我要哪一部分数据”。JSONPath 的语法最短,表达能力也最弱,它没有聚合、没有排序函数,复杂计算还是要拿回代码里做。

把它当成一个取值工具就好,别指望它替代查询引擎。数据量大到需要过滤、聚合、关联的时候,应该考虑把 JSON 落到数据库或者用专门的分析工具。

怎么快速验证一条路径

路径写错时往往不报错,只是返回空。靠肉眼对着一大段 JSON 找问题很费劲。可以先把数据格式化,看清层级,再把路径贴进在线工具里试,改一个符号就能立刻看到结果。

调试时从短路径开始,$ 加上第一层字段,确认有值再往后加。一次性写完整条路径,出错时很难判断是哪一段的问题。

常见问题 FAQ

JSONPath 和 XPath 有什么区别?

XPath 处理的是 XML 节点树,JSONPath 处理的是 JSON 的值。写法上 JSONPath 用 $ 表示根,用点号或方括号取字段,用 .. 做递归查找,语法比 XPath 短很多。JSONPath 没有正式标准,不同库对过滤表达式和函数扩展的支持并不一致,换库之前要重新测一遍。

递归下降 $.. 为什么容易变慢?

因为它会从根节点开始遍历整棵 JSON 树,每个对象和数组都要走一遍。数据只有几百 KB 时感觉不出来,遇到几 MB 的接口响应或者嵌套很深的日志,耗时会明显上升。能确定层级时就写完整路径,把递归下降留到结构不确定的场景。

键名里有空格或者点怎么办?

用方括号加引号,写成 $['user name'] 或 $['a.b']。点号写法会把点当成层级分隔符,遇到这种键名一定取不到值。方括号写法在键名包含中文、空格、横线时同样适用。

过滤表达式里字符串为什么要加引号?

因为不加引号会被当成字段名或者变量,而不是字符串值。比如 [?(@.status == 'ok')] 里 ok 必须带引号。数字和布尔值不用加,[?(@.price < 100)]、[?(@.active == true)] 都是合法写法。另外要注意 100 和 "100" 不是一回事,类型不一致时比较会失败。