Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした