LoginSignup
35
25

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-12

はじめに

この記事は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

参考

35
25
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
25