Go
golang
JSON
GoDay 12

動的な要素を持つJSONをいい感じにUnmarshalする

はじめに

この記事はGo Advent Calendar 2017の12日目の記事です。

JSON APIのクライアントを作ってるとき、ある要素が動的な値を取るJSONをUnmarshalしたいときがあります。
例えば下記のようなJSONです。
shapeという要素の値が動的に変わるJSONを仮定してます。

{
    "id": "001",
    "type": "circle",
    "shape": {
        "radius": 5
    }
}
{
    "id": "002",
    "type": "rectangle",
    "shape": {
        "height": 5,
        "width": 2
    }
}

こんなJSONをいい感じにUnmarshalする方法について、簡単に紹介させて頂きます。

また、今回のサンプルプログラムは以下においてあります。
https://play.golang.org/p/XMZRV1E7FX

要点

構造体の動的な要素はInterfaceで定義する

動的な要素はInterfaceで定義して、任意の構造体をが入っても同じ手段でアクセス可能にします。

type Drawer interface {
    Draw() string
}

type Figure struct {
    Id    string
    Type  string
    Shape Drawer
}

今回、動的な要素として扱うCircleとRectangleは適当にInterfaceを満たす実装をしています。

type Circle struct {
    Radius int
}

type Rectangle struct {
    Height int
    Width  int
}

func (c *Circle) Draw() string {
    msg := fmt.Sprintf("draw with radius %s", c.Radius)
    return msg
}

func (r *Rectangle) Draw() string {
    msg := fmt.Sprintf("draw with height %s, width %s", r.Height, r.Width)
    return msg
}

Unmarshalする際にエイリアスを定義して動的な要素をjson.RawMessageで上書きする

このままFigureをUnmarshalするとエラーになります。
そこでFigureのUnmarshalJSON内でエイリアスを定義して、動的な要素をjson.RawMessageで上書きします。

type Alias Figure
a := &struct {
    Shape json.RawMessage
    *Alias
}{
    Alias: (*Alias)(f),
}

そしてエイリアスを使ってUnmarshalします。


if err := json.Unmarshal(data, &a); err != nil {
    return err
}

するとエイリアスが動的な要素をjson.RawMessageで受け取ってくれるので、後はタイプを判別して動的な要素を適切な構造体にUnmarshalした後、オリジナルの構造体に渡してあげます。

switch f.Type {
case "circle":
    var c Circle
    if err := json.Unmarshal(a.Shape, &c); err != nil {
        return err
    }
    f.Shape = &c
case "rectangle":
    var r Rectangle
    if err := json.Unmarshal(a.Shape, &r); err != nil {
        return err
    }
    f.Shape = &r
default:
    return fmt.Errorf("unknown type: %q", f.Type)
}

UnmarshalJSONの全体像はこんな感じです。

func (f *Figure) UnmarshalJSON(data []byte) error {
    type Alias Figure
    a := &struct {
        Shape json.RawMessage
        *Alias
    }{
        Alias: (*Alias)(f),
    }
    if err := json.Unmarshal(data, &a); err != nil {
        return err
    }

    switch f.Type {
    case "circle":
        var c Circle
        if err := json.Unmarshal(a.Shape, &c); err != nil {
            return err
        }
        f.Shape = &c
    case "rectangle":
        var r Rectangle
        if err := json.Unmarshal(a.Shape, &r); err != nil {
            return err
        }
        f.Shape = &r
    default:
        return fmt.Errorf("unknown type: %q", f.Type)
    }
    return nil
}

まとめ

以上です。こんな感じで書くと、FigureをUnmarshalした際、適切な構造体がShapeの中に入ってDraw()によってアクセス可能になります。

最後に、はじめににリンク貼ったサンプルをマルっと貼っつけときます。


package main

import (
    "encoding/json"
    "fmt"
    "log"
)

const circle = `
{
    "id": "001",
    "type": "circle",
    "shape": {
        "radius": 5
    }
}
`

const rectangle = `
{
    "id": "002",
    "type": "rectangle",
    "shape": {
        "height": 5,
        "width": 2
    }
}
`

type Drawer interface {
    Draw() string
}

type Figure struct {
    Id    string
    Type  string
    Shape Drawer
}

type Circle struct {
    Radius int
}

type Rectangle struct {
    Height int
    Width  int
}

func main() {
    var c, r Figure
    if err := json.Unmarshal([]byte(circle), &c); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("circle %s\n", c.Shape.Draw())

    if err := json.Unmarshal([]byte(rectangle), &r); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("rectangle %s\n", r.Shape.Draw())
}

func (c *Circle) Draw() string {
    msg := fmt.Sprintf("draw with radius %d", c.Radius)
    return msg
}

func (r *Rectangle) Draw() string {
    msg := fmt.Sprintf("draw with height %d, width %d", r.Height, r.Width)
    return msg
}

func (f *Figure) UnmarshalJSON(data []byte) error {
    type Alias Figure
    a := &struct {
        Shape json.RawMessage
        *Alias
    }{
        Alias: (*Alias)(f),
    }
    if err := json.Unmarshal(data, &a); err != nil {
        return err
    }

    switch f.Type {
    case "circle":
        var c Circle
        if err := json.Unmarshal(a.Shape, &c); err != nil {
            return err
        }
        f.Shape = &c
    case "rectangle":
        var r Rectangle
        if err := json.Unmarshal(a.Shape, &r); err != nil {
            return err
        }
        f.Shape = &r
    default:
        return fmt.Errorf("unknown type: %q", f.Type)
    }
    return nil
}


実行結果
circle draw with radius 5
rectangle draw with height 5, width 2

参考