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

# はじめに

この記事は[Go Advent Calendar 2017](https://qiita.com/advent-calendar/2017/go)の12日目の記事です。

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

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

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

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


# 要点

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

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

```go
type Drawer interface {
	Draw() string
}

type Figure struct {
	Id    string
	Type  string
	Shape Drawer
}
```

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

```go
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`で上書きします。

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

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

```go

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

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

```go
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`の全体像はこんな感じです。

```go
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()`によってアクセス可能になります。

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

```go

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
```


## 参考
- http://eagain.net/articles/go-dynamic-json/
- http://choly.ca/post/go-json-marshalling/