• 推荐一个兼容性更好持续维护的JSONPath库
  • 发布于 2个月前
  • 98 热度
    0 评论
  • 离人愁
  • 0 粉丝 25 篇博客
  •   
现在用 Go 写业务的越来越多了,虽然没有干倒 JAVA,但抢占了很多 PHP、Node、Python Web 的份额。不过由于 Go 相对来说时间较短,在生态上差一点,有些场景一下找不到现成的库。因此我打算出个Go“冷门”库系列,介绍一些项目中实际验证过的、但鲜为人知的库。期望更多人了解到这些库,使用后多给开源社区反馈。

在 JAVA 时代,XmlPath 特别流行,对应的就有 JSONPath,方便直接获取 JSON 中的值。在 Go 里有个特别出名的库 gjson 就是这个用处,但它没有按 JSONPath 规范实现,单独用没有问题,但要和别的模块(非Go)交互时就得使用大家都有的。json-path-comparison 上倒是有几款 Go 的库,但很多都年久失修,就剩 ohler55/ojg 和 spyzhov/ajson 还在维护,当时选择了 ohler55/ojg,因为作者一直在做JSON相关的库,有成熟的 Ruby、C 的实现,看着经验非常丰富。

ojg 这个库,不仅仅只有 JSONPath 的功能还包含常规 JSON 库的 Marshal/Unmarshal,但这个赛道太卷了,后面有机会再介绍。这里主要介绍其 JSONPath 功能。使用上也很简单:
obj, err := oj.ParseString(`{
    "a":[
        {"x":1,"y":2,"z":3},
        {"x":2,"y":4,"z":6}
    ]
}`)
x, err := jp.ParseString("a[?(@.x > 1)].y")
results := x.Get(obj)
// returns [4]
oj.ParseString 是将 JSON 字符串解析为 Go 内置结构,等价于 Unmarshal 为 any(你用其它库进行这一步也是可以的)。但和一般 JSON 库默认的行为不一样,会判断类型,特别是 int64(其它库默认是float64),转换为对应的数据类型:
bool
int64
float64
string
time.Time
[]any
map[string]any
jp.ParseString 则是生成提取规则(可以重复使用),应用于前面解析的 any 上,进行提取。用法可以说是非常简单。

特别的,按 JSONPath 设计,返回的结果是个数组,即使明确提取出来的是一个值,就像上面的例子,直觉的结果应该是4,不过所有库都这样。但如果就想返回4呢?不能直接取[0],因为JSONPath是支持切片操作的,list[0:1]仍然需要返回一个list。那如何判断了,好在 jp.ParseString 的规则有类型,可以判断最后一步操作是不是切片等。
if len(results) == 1 && !isGetSlice(x) {
    return results[0]
}
 
func isGetSlice(expr jp.Expr) bool {
    if len(expr) > 0 {
        switch expr[len(expr)-1].(type) {
        case jp.Slice, jp.Union:
            return true
        default:
            return false
        }
    }
 
    return false
}
另外,因为 JSONPath 设计比较粗糙或者说很多细节不清晰,所有很多库可能会不一致,比如我提的这个 issue,JSON key 中有 - 就必须用[]方式(["foo-bar"])提取了,不能用 .foo-bar。
func Test_parse(t *testing.T) {
    //jsonStr := `{"foo-bar": "test"}`
 
    _, err := jp.ParseString(`["foo-bar"]`)
    if err != nil {
        t.Error(err)
    }
}
所以相对 JSONPath 我更推荐 jmespath,它除了功能更强大,另外一个优点就是规范定义比较清晰,各个语言的库都是官方主导实现,这样更方便跨语言交互。不过 Go 的实现推荐使用 jmespath-community/go-jmespath 版本,还在持续维护。
用户评论