interface要素を持つstructへのJSON Unmarshal

ちょっとやり方が分からなくて調べたのでメモ。

例題

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を使ってその要素を持つJSONhoge 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
}

とにかくfugapiyoもどっちも使って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された。

まぁこんなのが必要になる場合はそもそものデータ設計が間違ってると思うけど。

参照