備忘録。
GoのjsonパッケージにはMarshalerと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の箇所だけ独自の変換が行われます。
サンプル
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
補足
今回はEmbeddedのアプローチでMarshalJSONを実装しましたが、Defined Typeや親の構造体でインターフェースを実装しても良いかと思います。ただし、Defined Typeの場合は、time.Timerのメソッドを呼び出す為に毎回型変換が必要になることに注意が必要です。また、親の構造体の場合は、他のフィールドも纏めてJSON.Marshal/Unmarshalを定義することができる点がメリット(一括性)、またはデメリット(面倒、子のMarshal/Unmarshal定義を流用できない)にもなり得ます。
階層的な構造体をフラットにMarshalする
~~誰得ですが、~~可変長な配列や階層的になっているGoのオブジェクトを、JSONに変換するときにフラットにすることができます。MarshalJSON内でmap[string]interface{}を間に挟むことで、キー名を動的に指定してます。
サンプル
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))
}
出力結果
{"info_title":"title","item_0_str":"hoge","item_0_val":100,"item_1_str":"fuga","item_1_val":50}
Go Playground
備考
フラットにした分、Unmarshal時は、Unmarshalerインターフェースを実装して階層情報を変数名から解釈する必要があります。正規表現を使う流れになると思いますが、本記事では省略します。
まとめ
MarshalJSONとUnmarshalJSONを実装することで、標準のjson.Marshalとjson.Unmarshalを経由して独自に変換できるようになることがメリットの1つだと思います。また、うまく使えばJSON変換用の構造体とデータモデル用の構造体を1つに纏められるのが期待できそうです。