はじめに
encoding/json
パッケージを利用してJSONオブジェクトをパースする際に、JSON文字列の順序の通りのスライスに変換する方法をご紹介します。
TL;DR
- json.Unmarshalerを実装してDecoder.Token()で順次変換する
- 雑にパースするためのライブラリを作ったのでご利用ください (reference)
- 新しく作るものではではJSONオブジェクトに順序をもとめてはいけない
モチベーション
既存のAPIレスポンスなどでキーの順序に意味のあるJSONオブジェクトを扱うため、スライスとしてパースする機会がありました。
当初は map[string]*Value
のようなmapとしてパースすることを検討しましたが、Goのmapは順序が保証されない(ランダムにソートされる可能性がある)ためスライスに変換後、ソートするなどしていましたが、ソート可能なキーが存在しないケースがありました。
レスポンス例
{
"value1": 42,
"value2": 99
}
上記のようなフォーマットで順序に意味がありました。
json.Decoder
を利用する
json.Decoder の Token()
メソッドで順次処理することで、JSON文字列の順序通りに扱うことができます。
// bs := []byte(`{"value1":42,"value2":99}`)
decoder := json.NewDecoder(bytes.NewReader(bs))
for {
token, err := decoder.Token()
if err != nil {
if err != io.EOF {
fmt.Println(err)
}
return
}
fmt.Printf("%T: %v\n", token, token)
}
Output
json.Delim: {
string: value1
float64: 42
string: value2
float64: 99
json.Delim: }
とはいえデリミタの判定とか毎回するのは面倒...
雑に利用するためのパッケージをつくったのでぜひご利用ください
github.com/kamiaka/go-jsonutil
使い方
func UnmarshalEntries(json []byte, resolver func(key string, data []byte) error)
UnmarshalEntriesでは第1引数で受け取ったJSONをパースして、エントリごとに第2引数にキーと値のペアを渡します。(エラーが返った場合中断してエラーを返します)
type IntEntry struct { Key string; Value int }
var entries []*IntEntry
jsonutil.UnmarshalEntries([]byte(`{"a":1,"b":2,"c":3}`), func(key string, data []byte) error {
entry := &IntEntry{
Key: key,
}
entries = append(entries, entry)
return json.Unmarshal(data, &entry.Value)
})
json.Unmarshalerの実装内にて呼び出すことで、再帰的なパースも可能です。
type IntEntries []*IntEntry
func (entries *IntEntries) UnmarshalJSON(bs []byte) error {
return jsonutil.UnmarshalEntries(bs, func(key string, data []byte) error {
entry := &IntEntry{
Key: key,
}
*entries = append(*entries, entry)
return json.Unmarshal(data, &entry.Value)
})
}
type X struct {
Foo string `json:"foo"`
Entries IntEntries `json:"entries"`
}
func main() {
var x *X
err := json.Unmarshal([]byte(`{"foo":"Foo!","entries":{"a":42,"b":99}}`), &x)
fmt.Printf("%#v, error: %v", x, err)
// Output:
// &main.X{Foo:"Foo!", Entries:main.IntEntries{(*main.IntEntry)(0xc00000c220), (*main.IntEntry)(0xc00000c2a0)}}, error: <nil>
}
詳しくは ドキュメント をご覧ください。
さいごに
あくまで既存APIのレスポンスなどでJSONオブジェクトの順序に意味があった際に順序をそのまま扱うためのものです。
ほとんどの言語において連想配列に順序はありません。
新規作成するAPIなどで順序に意味をもたせる場合は配列にするなどしてください。