Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

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

More than 3 years have 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

参考

kanga
うさんくさいインフラエンジニア
http://kangaat.hatenablog.com/
speee
株式会社Speeeは「解き尽くす。未来を引きよせる。」というミッションを実現すべく、中長期的な目線で企業価値を最大化させていくため、組織・事業のStyleを大切にした永続的な価値創造を目指しています。
https://www.speee.jp/
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