GolangのMarshalerとUnmarshalerを使って複雑なJSON変換をするTips

備忘録。

Goのjsonパッケージには[Marshaler](https://golang.org/pkg/encoding/json/#Marshaler)と[Unmarshaler](https://golang.org/pkg/encoding/json/#Unmarshaler)という独自の変換方式を定義できるinterfaceが用意されてますので、これを実装することで複雑なJSON↔構造体の変換をある程度簡単に行うことができます。

既にJSONスキーマが決まっている中で、プログラム側に都合の良い変換を行いたい場合は参考になるかもです。

## 構造体のフィールドを指定の形式でMarshal, Unmarshalする

下記のサンプルコードは、Goではtime.Time(を埋め込んだUnixTime)型で扱いつつ、JSONの相互変換時はunixtimeにする実装になってます。

UnixTimeの`MarshalJSON`と`UnmarshalJSON`がそれぞれMarshaler,Unmarshalerインターフェースの実装メソッドになります。

main関数内で、UnixTimeを内包するStructに対してjson.Marshal, json.Unmarshalをかけていますが、全体は既存の変換方式に則りつつ、UnixTimeの箇所だけ独自の変換が行われます。

**サンプル**

```go
package main

import (
	"encoding/json"
	"fmt"
	"strconv"
	"time"
)

type UnixTime struct {
	time.Time
}

func (ut UnixTime) MarshalJSON() ([]byte, error) {
	return []byte(
		strconv.FormatInt(ut.Unix(), 10),
	), nil
}

func (ut *UnixTime) UnmarshalJSON(data []byte) error {
	var t int64
	if err := json.Unmarshal(data, &t); err != nil {
		return err
	}
	ut.Time = time.Unix(t, 0)
	return nil
}

type Struct struct {
	UnixTime UnixTime `json:"unix_time"`
}

func main() {
	js := []byte(`{"unix_time":1257894000}`)
	fmt.Println("JSON:", string(js))

	var str Struct
	if err := json.Unmarshal(js, &str); err != nil {
		fmt.Println(err)
	}
	fmt.Printf("Unmarshal: Time=%s\n", str.UnixTime)

	js, err := json.Marshal(str)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println("Marshal:", string(js))
}
```

**実行結果**

```
JSON: {"unix_time":1257894000}
Unmarshal: Time=2009-11-10 23:00:00 +0000 UTC
Marshal: {"unix_time":1257894000}
```

**Go Playground**

https://play.golang.org/p/elgRiCtbdHw

**補足**

今回はEmbeddedのアプローチでMarshalJSONを実装しましたが、Defined Typeや親の構造体でインターフェースを実装しても良いかと思います。ただし、Defined Typeの場合は、time.Timerのメソッドを呼び出す為に毎回型変換が必要になることに注意が必要です。また、親の構造体の場合は、他のフィールドも纏めてJSON.Marshal/Unmarshalを定義することができる点がメリット（一括性）、またはデメリット（面倒、子のMarshal/Unmarshal定義を流用できない）にもなり得ます。

##  階層的な構造体をフラットにMarshalする

~~誰得ですが、~~可変長な配列や階層的になっているGoのオブジェクトを、JSONに変換するときにフラットにすることができます。MarshalJSON内でmap[string]interface{}を間に挟むことで、キー名を動的に指定してます。

**サンプル**

```go
package main

import (
	"encoding/json"
	"fmt"
)

type Struct struct {
	Info     Info
	ItemList []Item
}

type Info struct {
	Title string
}

type Item struct {
	Str string
	Val int
}

func (s Struct) MarshalJSON() ([]byte, error) {
	m := map[string]interface{}{}
	for i, v := range s.ItemList {
		m[fmt.Sprintf("item_%d_str", i)] = v.Str
		m[fmt.Sprintf("item_%d_val", i)] = v.Val
	}
	m["info_title"] = s.Info.Title
	return json.Marshal(m)
}

func main() {
	m := &Struct{
		Info: Info{
			Title: "title",
		},
		ItemList: []Item{
			{Str: "hoge", Val: 100},
			{Str: "fuga", Val: 50},
		},
	}
	js, err := json.Marshal(m)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(string(js))
}
```

**出力結果**

```json
{"info_title":"title","item_0_str":"hoge","item_0_val":100,"item_1_str":"fuga","item_1_val":50}
```

**Go Playground**

https://play.golang.org/p/VxgPgAVTXBi

**備考**

フラットにした分、Unmarshal時は、Unmarshalerインターフェースを実装して階層情報を変数名から解釈する必要があります。正規表現を使う流れになると思いますが、本記事では省略します。

## まとめ

MarshalJSONとUnmarshalJSONを実装することで、標準のjson.Marshalとjson.Unmarshalを経由して独自に変換できるようになることがメリットの１つだと思います。また、うまく使えばJSON変換用の構造体とデータモデル用の構造体を１つに纏められるのが期待できそうです。