(Golang) ネスト&interfaceを使った struct を Unmarshal で JSON から読み取る

ちょっとハマったのでメモ。

こういう JSON で書かれた設定ファイルがあります。

{
	"ActionConfig": [
		{
			"Name": "hogehoge",
			"Type": "Pixela",
			"Config": {
				"UserID": "tsubasaxZZZ",
				"GraphID": "graph1",
				"Secret": "SECRET"
			}
		},
		{
			"Name": "pgr",
			"Type": "Slack",
			"Config": {
				"UserID": "tsubasaxZZZ",
				"ChannelID": "testchannel"
			}
		}
	]
}

この時、ActionConfig の Type によって、Config の中身を変えたいです。
これを Go の struct と interface でこんな感じで表現してみました。

type Configs struct {
	ActionConfigs []ActionConfig `json:"ActionConfig"`
}
type ActionConfig struct {
	Name   string   `json:"Name"`
	Type   string   `json:"Type"`
	Config Actioner `json:"Config"`
}
type Actioner interface {
	PostEvent(metadata MetaData) error
}

で、この時に、json.Unmarshal をするとエラーになります。

どうするかというと、json.UnmarshalJSON 関数を使って、自分で実装すればよいということが分かりました。

こちらを参考にしました。
memo.sugyan.com
qiita.com

ただ、今回の場合、

type Configs struct {
	ActionConfigs []ActionConfig `json:"ActionConfig"`
}

の部分で、配列にしていて&埋め込みでネストが一つ深くなっていて少し悩みました。

最終的にこんな感じにしてみました。

struct ごとに UnmarshalJSON を作ったのと、map で Type と struct をマッピングしたのが肝です。

package sechecker

import "encoding/json"

type Actioner interface {
	PostEvent(metadata MetaData) error
}

type ActionConfig struct {
	Name   string   `json:"Name"`
	Type   string   `json:"Type"`
	Config Actioner `json:"Config"`
}

type Configs struct {
	ActionConfigs []ActionConfig `json:"ActionConfig"`
}

func (c *Configs) UnmarshalJSON(b []byte) error { <--------------------- 1階層目の Configs 用の UnmarshalJSON
	type alias Configs
	a := struct {
		*alias
	}{
		alias: (*alias)(c),
	}
	if err := json.Unmarshal(b, &a); err != nil {
		return err
	}
	return nil
}

var (
	_configObj = map[string]Actioner{ <--------------------- Type で struct をマッピング
		// Pixela 用のコンフィグ
		"Pixela": &PixelaConfig{},
	}
)

func (c *ActionConfig) UnmarshalJSON(b []byte) error {<--------------------- 2階層目の ActionConfig 用の UnmarshalJSON
	type alias ActionConfig
	a := struct {
		Config json.RawMessage `json:"Config"`
		*alias
	}{
		alias: (*alias)(c),
	}
	if err := json.Unmarshal(b, &a); err != nil {
		return err
	}

	// Type から型情報を判別しマップから取得
	// 取得できないときは nil
	o, ok := _configObj[c.Type]
	if ok {
		if err := json.Unmarshal(a.Config, &o); err != nil {
			return err
		}
	}
	c.Config = o
	return nil
}

Configs structに対する UnmarshallJSON しないといけないのと、コンフィグが増えたらマップに足さないといけないのはちょっとイケてないですね。。
他にいい方法がありそうです。