LoginSignup
1
0

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-07-19

備忘録。

GoのjsonパッケージにはMarshalerUnmarshalerという独自の変換方式を定義できるinterfaceが用意されてますので、これを実装することで複雑なJSON↔構造体の変換をある程度簡単に行うことができます。

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

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

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

UnixTimeのMarshalJSONUnmarshalJSONがそれぞれ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つに纏められるのが期待できそうです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0