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