メソッドを持った interface を要素に持つ struct への JSON Unmarshal

April 10, 2019

go

interface要素を持つstructへのJSON Unmarshal - すぎゃーんメモ

これが参考になった。

ただ、このケースで上げているのは interface がどの struct で評価されればいいかわかっているケースだった。 例えば、これをキーに持つ JSON だった場合は struct A で、このキーがなかったら struct B で、みたいなケースは自分で JSON の中を読みにいって判別して Unmarshal する他ない。

具体例を示す。

type State struct {
	Modules []Module `json:"modules"`
}

type Module struct {
	Name      string     `json:"name"`
	Resources []Resource `json:"resources"`
}

// ちなみにメソッドを持っていない場合は
// interface{} として Unmarshal されるのでエラーにならない
type Resource interface {
	Get()
	// ...
}

type AWSModule struct {
	Name string `json:"name"`
}

func (m AWSModule) Get() {}

type GCPModule struct {
	Name    string `json:"name"`
	Project string `json:"project"`
}

func (m GCPModule) Get() {}

こういう状況だと上のブログにもある通り、

err := json.NewDecoder(file).Decode(&state)  // state contains Module
// json: cannot unmarshal object into Go struct field Module.resources of type main.Resource

ここでエラーが返る。

この場合 interface になっている Resource を要素に持つ struct である Module に対して UnmarshalJSON を実装する。

func (s *Module) UnmarshalJSON(b []byte) error {
	m := map[string]json.RawMessage{}
	if err := json.Unmarshal(b, &m); err != nil {
		return err
	}
	for k, v := range m {
		switch k {
		case "resources":
			var resources []map[string]interface{}
			err := json.Unmarshal([]byte(v), &resources)
			if err != nil {
				return err
			}
			for _, resource := range resources {
				if _, ok := resource["project"]; ok {
					var m GCPModule
					resource, err := json.Marshal(resource)
					if err != nil {
						return err
					}
					err = json.Unmarshal(resource, &m)
					if err != nil {
						return err
					}
					s.Resources = append(s.Resources, m)
				} else {
					var m AWSModule
					resource, err := json.Marshal(resource)
					if err != nil {
						return err
					}
					err = json.Unmarshal(resource, &m)
					if err != nil {
						return err
					}
					s.Resources = append(s.Resources, m)
				}
			}
		default:
		}
	}
	return nil
}

コードの見通しはおいておいて、実現したいコード全体がこれ。

case の中が肝になっている。一旦 []map[string]interface{} でマップにして GCPModule 固有の project をキーに落とし込むべき struct を判定している。 あとは Marshal してマップをバイト列にして、Unmarshal で struct にしている (map to struct)。 ちなみに、先のブログにもある通り、これは他の要素も UnmarshalJSON してあげないと、default で抜けていくのでその要素がゼロ値になった struct になってしまう。struct の要素が多い場合は、紹介されていた alias を使うパターンで書いたほうがよい。

また、サーバレスポンスなどがパラメータやイベントによって動的に変わる場合に、その要素の型を json.RawMessage として struct に埋め込んで実装した UnmarshalJSON で遅延評価する場合に json.RawMessage が役立つケースが多いようだった。

例: Go で構造の一部が動的に変わる JSON を扱いたい – Naomichi Agata – Medium