ちょっとやり方が分からなくて調べたのでメモ。
例題
type hoge struct { Foo int `json:"foo"` Bar baz `json:"bar"` } type baz interface { hoge() } type fuga struct { Fuga string `json:"fuga"` } func (*fuga) hoge() {}
という感じで "foo"
は int
に固定されているけど "bar"
は baz
interface というのだけ定義されている hoge
struct がある。
baz
interface を正しく実装している fuga
structを使ってその要素を持つJSONをhoge
structにunmarshalしたい。とする。
func main() { str := `{"foo":3,"bar":{"fuga":"fuga"}}` var h hoge if err := json.Unmarshal([]byte(str), &h); err != nil { log.Fatal(err) } }
これは失敗する。
2018/06/23 22:42:58 json: cannot unmarshal object into Go struct field hoge.bar of type main.baz exit status 1
"bar"
要素のunmarshalの方法が分からん。ということなので明示的に教えてやる必要がある。
hoge
struct に UnmarshalJSON([]byte) error
関数を実装してやる。
mapを使う方法
直感的に分かりやすい? かも知れない方法
func (h *hoge) 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 "foo": var i int if err := json.Unmarshal(v, &i); err != nil { return err } h.Foo = i case "bar": var f fuga if err := json.Unmarshal(v, &f); err != nil { return err } h.Bar = &f } } return nil }
一旦 map[string]json.RawMessage{}
にマッピングしてunmarshalしてから、各要素をfor loopで回して それぞれの値を改めてunmarshalする。 他のfieldにunmarshalすべきtypeの情報が入っていればここで取り出してから分岐させたりして処理すれば良い。
func main() { str := `{"foo":3,"bar":{"fuga":"fuga"}}` var h hoge if err := json.Unmarshal([]byte(str), &h); err != nil { log.Fatal(err) } log.Printf("%#v, (bar: %+v)", h, h.Bar) }
$ go run main.go 2018/06/23 22:52:12 main.hoge{Foo:3, Bar:(*main.fuga)(0xc42000e2c0)}, (bar: &{Fuga:fuga})
alias typeを使う
上記の方法だと Foo
要素とか interface
以外のものもいちいち分岐の中でunmarshal処理を書かなければならず、めんどい。
そこで、aliasなtypeを使って処理する方法があるようだ。
func (h *hoge) UnmarshalJSON(b []byte) error { type alias hoge a := struct { Bar json.RawMessage `json:"bar"` *alias }{ alias: (*alias)(h), } if err := json.Unmarshal(b, &a); err != nil { return err } var f fuga if err := json.Unmarshal(a.Bar, &f); err != nil { return err } h.Bar = &f return nil }
このようにalias typeを使って そこに突っ込むようにunmarshalすると、Bar
だけ json.RawMessage
で受け取り それ以外はそのままunmarshalしてくれる、ようだ。
なので Bar
要素の部分だけ明示的に処理を書けば良いらしい。
スッキリ書けて良さそう。
複数のtype候補がある場合
例えば上記の fuga
struct 以外にも 似たような piyo
struct が同様に baz
interface を実装していたとする。
type hoge struct { Foo int `json:"foo"` Bar baz `json:"bar"` } type baz interface { hoge() } // こっちのFugaはstring type fuga struct { Fuga string `json:"fuga"` } func (*fuga) hoge() {} // こっちのFugaはbool type piyo struct { Fuga bool `json:"fuga"` } func (*piyo) hoge() {}
同名のフィールドを持っているが型が違うので、
{"foo":3,"bar":{"fuga":"true"}}
というJSONは "fuga"
は文字列なので fuga
structで、
{"foo":3,"bar":{"fuga":true}}
というJSONの場合は "fuga"
がbooleanを受け取るので piyo
struct にしたい。
どっちか分からない、という場合は 「上手くunmarshalできるかどうかとにかく試してみる」という感じになりそう
func (h *hoge) UnmarshalJSON(b []byte) error { type alias hoge a := struct { Bar json.RawMessage `json:"bar"` *alias }{ alias: (*alias)(h), } if err := json.Unmarshal(b, &a); err != nil { return err } var ( f fuga p piyo ) for _, c := range []baz{&f, &p} { err := json.Unmarshal(a.Bar, c) if err != nil { if _, ok := err.(*json.UnmarshalTypeError); ok { continue } return err } h.Bar = c break } return nil }
とにかくfuga
もpiyo
もどっちも使ってjson.Unmarshal
を試みる。文字列なのにboolとしてunmarshalしようとしたりその逆だったりした場合 json.UnmarshalTypeError
が返ってくるので、その場合は別のtypeで再挑戦、みたいな感じ。
func main() { str1 := `{"foo":3,"bar":{"fuga":"true"}}` str2 := `{"foo":3,"bar":{"fuga":true}}` var h hoge if err := json.Unmarshal([]byte(str1), &h); err != nil { log.Fatal(err) } log.Printf("%#v, (bar: %+v)", h, h.Bar) if err := json.Unmarshal([]byte(str2), &h); err != nil { log.Fatal(err) } log.Printf("%#v, (bar: %+v)", h, h.Bar) }
$ go run main.go 2018/06/23 23:16:47 main.hoge{Foo:3, Bar:(*main.fuga)(0xc42000e2a0)}, (bar: &{Fuga:true}) 2018/06/23 23:16:47 main.hoge{Foo:3, Bar:(*main.piyo)(0xc420014288)}, (bar: &{Fuga:true})
ちゃんと1つ目はfuga
としてunmarshalされ 2つ目はpiyo
としてunmarshalされた。
まぁこんなのが必要になる場合はそもそものデータ設計が間違ってると思うけど。