spyzhov/ajson を使ってGolangでJSONPathとかJSONの組み立てとか
GolangでJSONPathを使う必要がありライブラリを探していたところ、spyzhov/ajson が便利でした。spyzhov/ajson を使ってJSONPathを使ったりJSONを組み立てたりする方法についてまとめます。
JSONPath を使う
JSONPathとは、JSONの一部分を取り出したり、値同士の演算を行ったりするためのDDLです。JSONPathは、kubectlでのオプションの指定、AWS Step Functionsでの入出力のフィルタリングなどに使われいます。
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/json
のUnmarshal
関数ではデコード結果をInterfaceに書き込むのに対して、ajson.Unmarshal(data)
はajson.Node
を返します。
ajson package - github.com/spyzhov/ajson - pkg.go.dev
type Node
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"}
この方法ですと、連結するデータが可変の場合には対応できませんし、連結元のデータ (v1
やv2
)のフィールドを変更する際には書き換える箇所が多くなり大変です。
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であることを事前に確認し、然るべきエラー処理をするのが良いでしょう。