golang は ゆるふわに JSON を扱えまぁす!

golang でゆるふわに JSON を扱う方法を簡単に紹介します。

以下のツイートにもある通り、一見 golang は struct を定義しないと JSON を扱えないように見えます。他にも似たようなツイートをチラホラと見かけましたが、それらは全部誤解です。そこでこの記事では、golang でゆるふわ (structを定義せず) に JSON を扱う方法を紹介します。

この記事では入力の json.Unmarshal() を取り扱いますが、出力の json.Marshal() にも応用できます。

TL;DR

  1. json.Unmarshal() には *interface{} を渡せます
  2. interface{}dproxy を使うと、値のアクセスで楽ができます

json.Unmarshal()

encoding/json#Unmarshal にある例をみると、確かに一見 struct を渡さなければいけないように思えてしまいます。以下はそれに該当するコードの抜粋です。

var jsonBlob = []byte(`[
    {"Name": "Platypus", "Order": "Monotremata"},
    {"Name": "Quoll",    "Order": "Dasyuromorphia"}
]`)
type Animal struct {
    Name  string
    Order string
}
var animals []Animal
err := json.Unmarshal(jsonBlob, &animals)

(動作するコード)

ところが json.Unmarshal() の第2引数には *interface{} を渡すことができるのです。 結果、前述の例は下のようなコードに単純化できます。

var animals interface{}
err := json.Unmarshal(jsonBlob, &animals)

(動作するコード)

簡単なデータや、コンパイル時にスキーマを定義できない自由な形式の JSON を取り扱う際には、覚えておくと良いテクニックです。

interface{} から値を取り出す

json.Unmarshal()*interface{} を渡した場合には interface{} 型の値が得られるわけですが、その値の実際の型はなんなのでしょう?

前述のコードで出力部分を %#v にすれば型も確認できます。その出力結果は、整形すると以下のようになります。

[]interface {}{
    map[string]interface {}{
        "Name":  "Platypus",
        "Order": "Monotremata",
    },
    map[string]interface {}{
        "Name":  "Quoll",
        "Order": "Dasyuromorphia",
    },
}

つまり JSON の配列は []interface{} 型の値として、オブジェクトは map[string]interface{} 型の値として取り出せていることが、この出力から見て取れます。

よって 1番目の要素の Order の値を string 型で取り出す のであれば、以下の様なコードで実現できます。

s := animals.([]interface{})[0].(map[string]interface{})["Order"].(string)

念のため、これが何をやっているのかを解説しますと…次のコードを見てしまうほうが話は早いでしょう。

// animals を 配列 []interface{} にする / 型アサーション≒キャスト
a := animals.([]interface{})

// 1つ目の要素を interface{} として取り出す
b := a[0]

// 1つ目の要素を オブジェクト map[string]interface{} に型アサーション
c := b.(map[string]interface{})

// 要素から "Order" の値を取り出す
d := c["Order"]

// d は string ではなく interface{} なので、string に型アサーション
s := d.(string)

エラーハンドリングを省略していますが、これが golang でゆるふわに JSON を取り扱う基本になっています。

もっと楽に値を取り出す - dproxy

これまで見たように、golang では json.Unmarshal() した interface{} から値を取り出すのは、やや面倒くさい感じを拭えません。また前述のコード例ではエラーハンドリングを一切していません。そのため実際のプロダクトで使う場合には、型が異なっていた場合や名前が間違っていた場合などを考慮し、それらのハンドリングに多くのコードを必要とします。

しかしそれでは大変なので dproxy というパッケージを書きました。この dproxy を使うと、先ほどの 1番目の要素の Order の値を string 型で取り出す コードは以下のようになります。

s, err := dproxy.New(animals).A(0).M("Order").String()

たいしてコードの長さは変わっていませんが、戻り値に err が増えていることに注目してください。この err には、例えば JSON のルートが 配列じゃなかった場合や、要素に "Order" というキーが存在しなかった場合に、適切なエラーが返されます。もちろん、取り出した値が string 型でなかったときも同様です。

dproxy の使い方を簡単に解説しておきますと、まず dproxy.New()interface{} をラップして dproxy.Proxy インターフェース (以下 Proxy) を取得します。Proxy には2種類のメソッド群が提供されています。1つは interface{} を各型の値として取り出す Bool()String() といったメソッド群です。もう1つは interface{} をマップや配列として子要素を取り出し、その Proxy を返す M(string)A(int) です。

前者のメソッド群が値だけでなく error を返し、後者のメソッド群が Proxy を返すので、メソッドチェーンを形成して、実際に値を取り出す時に中間で発生したエラーを遅延評価できるように、良い感じになっています。以下、その例です。

_, err = dproxy.New(animals).A(0).M("foo").String()
fmt.Println(err)
// 次のようなエラーとなる: not found: [0].foo

_, err = dproxy.New(animals).A(2).M("Order").String()
fmt.Println(err)
// 次のようなエラーとなる: not found: [2]

_, err = dproxy.New(animals).A(0).M("Order").Bool()
fmt.Println(err)
// 次のようなエラーとなる: not matched types: expected=bool actual=string: [0].Order

また M(string)A(int) はそれぞれ Proxy を返すため、それを再利用することもできます。

a0 := dproxy.New(animals).A(0)

// 1番目の要素の Name と Order を string で取り出す
name, _ := a0.M("Name").String()
order, _ := a0.M("Order").String()

ね、超便利そうでしょ?

dproxy の詳細な API は godoc を参照してください。

おまけ: dproxy と同じコンセプトのもの

dproxy 同様に interaface{} に対して良い感じのアクセサーを提供する方法として、JSON Pointer (RFC6901) を用いた方法が、まっつんさん により mattn/go-pointer (以下 jsonpointer) として実装されています。

jsonpointer には RFC6901 として標準化されたクエリ文字列が利用できるメリットがあります。それに対して dproxy には中間クエリの結果を使いまわして、エラー評価を遅延させられるメリットがあります。また計測はしていませんがもしかしたら、クエリ文字列を解釈しない分 dproxy のほうが速い、ということもあるかもしれません。

まとめ

golang において JSON を取り扱う際には interface{} を使って、ゆるい感じにできることを紹介しました。また interface{} から任意の値を取り出す際には dproxy のようなパッケージを使うと楽できることを紹介しました。