spyzhov/ajson を使ってGolangでJSONPathとかJSONの組み立てとか

GolangでJSONPathを使う必要がありライブラリを探していたところ、spyzhov/ajson が便利でした。spyzhov/ajson を使ってJSONPathを使ったりJSONを組み立てたりする方法についてまとめます。

github.com

JSONPath を使う

JSONPathとは、JSONの一部分を取り出したり、値同士の演算を行ったりするためのDDLです。JSONPathは、kubectlでのオプションの指定、AWS Step Functionsでの入出力のフィルタリングなどに使われいます。

JSONPath Support | Kubernetes

Input and Output Processing in Step Functions - AWS Step Functions

以下はJSONPath式 $.bbbを用いてbbbというフィールドの値を取り出すサンプルです。

package main

import (
    "fmt"

    "github.com/spyzhov/ajson"
)

func main() {
    json := []byte(`{
  "aaa": "111",
  "bbb": "222",
  "ccc": "333"
}`)

    root, err := ajson.Unmarshal(json)
    if err != nil {
        panic(err.Error())
    }

    fmt.Println("=== raw json ===")
    fmt.Println(root)

    nodes, err := root.JSONPath("$.bbb")
    if err != nil {
        panic(err.Error())
    }

    fmt.Println("=== filtered result by JSONPath ===")
    for _, node := range nodes {
        fmt.Println(node)
    }
}
$ go run main.go
=== raw json ===
{
    "aaa": "111",
    "bbb": "222",
    "ccc": "333"
}
=== filtered result by JSONPath ===
"222"

JSONPath式$.bbbが適用され、フィールドbbbに対応する値である222が出力されていることがわかります。

このサンプルコードを少し詳しく見ていきましょう。

func Unmarshal

func Unmarshal(data []byte) (root *Node, err error)

標準パッケージ encoding/jsonUnmarshal関数ではデコード結果をInterfaceに書き込むのに対して、ajson.Unmarshal(data)ajson.Nodeを返します。

ajson package - github.com/spyzhov/ajson - pkg.go.dev

type Node

JSONデータを木構造として持つための構造体です。

ajson package - github.com/spyzhov/ajson - pkg.go.dev

定義は以下のようになっています。

type Node struct {
    parent   *Node
    children map[string]*Node
    key      *string
    index    *int
    _type    NodeType
    data     *[]byte
    borders  [2]int
    value    atomic.Value
    dirty    bool
}

またajson.NodeType型の定数として、JSONの値の型とGoの型の対応付けがされています。

const (
    // Null is reflection of nil.(interface{})
    Null NodeType = iota
    // Numeric is reflection of float64
    Numeric
    // String is reflection of string
    String
    // Bool is reflection of bool
    Bool
    // Array is reflection of []*Node
    Array
    // Object is reflection of map[string]*Node
    Object
)

このような定義によって、ajson.Nodeからstringの値を取り出したり、Arrayとして[]*Node型の値を取り出してイテレーションを回したりすることができます。

func JSONPath

func JSONPath(data []byte, path string) (result []*Node, err error)

dataで渡されたJSONに対してJSONPath式を適用して、結果を返す関数です。内部でfunc Unmarshalを呼び出しています。

func (*Node) JSONPath

func (n *Node) JSONPath(path string) (result []*Node, err error)

ajson.Node型の変数をレシーバとして、JSONPath式を適用する関数です。

(おまけ) JSONを組み立てる

いくつかのJSONを連結したJSONを出力したい、というような状況が稀にあります。例えば{"A": "111", "B": "222"}{"C": "333", "D": "444"}から、{"A": "111", "B": "222", "C": "333", "D": "444"}を得るような場合です。

単に文字列処理をしてもできないことはないですが、JSONパーサを新たに実装し、それに対する十分なテストを用意するコストは負いたくありません。

encoding/jsonを使って実装するならば、以下のようになるしょう。連結元のJSONを一度Unmarshalし、新たに定義した連結後のデータ型を使ってMarshalすることで連結しています。

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    var v1 struct {
        A, B string
    }
    if err := json.Unmarshal([]byte(`{"A": "111", "B": "222"}`), &v1); err != nil {
        panic(err.Error())
    }

    var v2 struct {
        C, D string
    }
    if err := json.Unmarshal([]byte(`{"C": "333", "D": "444"}`), &v2); err != nil {
        panic(err.Error())
    }

    v := struct {
        A, B, C, D string
    }{
        A: v1.A,
        B: v1.B,
        C: v2.C,
        D: v2.D,
    }

    b, err := json.Marshal(v)
    if err != nil {
        panic(err.Error())
    }

    fmt.Println(string(b))
}
$ go run main.go
{"A":"111","B":"222","C":"333","D":"444"}

この方法ですと、連結するデータが可変の場合には対応できませんし、連結元のデータ (v1v2)のフィールドを変更する際には書き換える箇所が多くなり大変です。

ajson.Nodeのメソッドを駆使すれば、綺麗にJSONを組み立てることができます。

package main

import (
    "fmt"

    "github.com/spyzhov/ajson"
)

func main() {

    // 連結したいJSONをこれにappendしていく
    var objects []map[string]*ajson.Node

    // データ1
    n1, err := ajson.Unmarshal([]byte(`{"A": "111", "B": "222"}`))
    if err != nil {
        panic(err.Error())
    }
    objects = append(objects, n1.MustObject())

    // データ2
    n2, err := ajson.Unmarshal([]byte(`{"C": "333", "D": "444"}`))
    if err != nil {
        panic(err.Error())
    }
    objects = append(objects, n2.MustObject())

    // 連結する
    root := ajson.ObjectNode("", map[string]*ajson.Node{})
    for _, object := range objects {
        for key, node := range object {
            if err := root.AppendObject(key, node); err != nil {
                panic(err.Error())
            }
        }
    }

    // 連結後のデータをMarshalする
    result, err := ajson.Marshal(root)
    if err != nil {
        panic(err.Error())
    }

    fmt.Println(string(result))
}
$ go run main.go
{"A":"111","B":"222","C":"333","D":"444"}

このようにすれば連結元のデータ数が可変の場合にも対応できます。また連結後のフィールド名も動的に決めることができます。

上記の例では簡単のために省略していますが、func (*Node) MustObjectはNodeTypeがObjectではない場合 (String"aaa"やArray[1, 2, 3]に対応するNodeなどの場合)にはpanicを発生させます。連結するJSONが定数でない場合にはfunc (*Node) IsObject を使って、NodeTypeがObjectであることを事前に確認し、然るべきエラー処理をするのが良いでしょう。

ajson package - github.com/spyzhov/ajson - pkg.go.dev

ajson package - github.com/spyzhov/ajson - pkg.go.dev