LoginSignup
1
0

More than 1 year has passed since last update.

Go言語でJSONオブジェクトの順序を維持したままスライスに変換する

Last updated at Posted at 2021-05-21

はじめに

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.DecoderToken() メソッドで順次処理することで、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などで順序に意味をもたせる場合は配列にするなどしてください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0