GoではJSONを扱うときでもしっかり型定義に当てはめて利用するのが一般的だ。
しかし、外部から受け取ったJSONデータは型に当てはめつつ併せて生データも保存しておきたいときがある。
Defind Type
をうまく使うとシンプルなUnmarshalJSON(data []byte)
メソッドを定義できる。
type Event struct {
ID string `json:"id"`
Type string `json:"type"`
Payload Payload `json:"pyload"`
// 構造体にマッピングする前のJSONを保存しておきたい
Raw json.RawMessage `json:"-"`
}
TL;DR
- 外部から受け取るJSONは構造が不意に変わることを想定したいときがある
UnmarshalJSON(data []byte)
メソッドを使うと独自のJSONパースができる- 構造体にマッピングすると、構造体に定義されていないデータが欠落する
- JSONの生データを扱うときは
json.RawMessage
を使うとよい Defind Type
で名前を変えると独自定義したUnmarshalJSON(data []byte)
メソッドを呼ばなくなる
https://play.golang.org/p/n7qvO3vTiV0
type Event struct {
ID string `json:"id"`
Type string `json:"type"`
Payload Payload `json:"pyload"`
Raw json.RawMessage `json:"-"`
}
func (e *Event) UnmarshalJSON(data []byte) error {
type event Event
var ee event
err := json.Unmarshal(data, &ee)
if err != nil {
return err
}
*e = (Event)(ee)
e.Raw = data
return nil
}
GoでJSONを扱うとき
GoではJSONを使うときもしっかり型を意識して取り扱う。
受け取ったJSONデータはencoding/json
パッケージを使ってデコードして構造体にマッピングする。
https://pkg.go.dev/encoding/json
UnmarshalJSON(data []byte)
メソッドを使うと独自のJSONパースができる
encoding/json
パッケージは通常構造体のフィールドに記載されたjson
タグを見て構造体にJSONのデータを当てはめていく。
しかし構造体に対してUnmarshalJSON(data []byte)
メソッドを定義しておくと、独自ロジックのJSONデコードが行える。
- JSONの変換をカスタマイズするメソッドを生成する | DeNA Codelab
JSONの生データを扱うときはjson.RawMessage
を使うとよい
また、JSONの元データを扱うときはjson.RawMessage
型を利用する。
外部から受け取るJSONは構造が不意に変わることを想定したいときがある
上記の仕組みを使ってJSONデータを使うとき、問題になることがある。 当然といえば当然なのだが、構造体に未定義のキーはJSONデータから構造体へマッピングするときに欠落する。 そのため、たとえば次のようなステップを踏んでいると、HTTPレスポンス中の未定義のキーのデータは喪失する。
- HTTPレスポンスをJSONで受け取る
- 構造体にデコードする
- DBに保存する
しっかりとした仕様の元でJSONを扱っていれば未定義のキーの心配などしなくてもよいのだが、お行儀の悪いAPIだと未定義のデータも混じる可能性が発生しうる。 そのため構造体にマッピングしつつJSON生データの保存するロジックを書きたい時がある。
生データを保存する独自UnmarshalJSON(data []byte)
メソッド
実装の完成形を先に書くと、このようなUnmarshalJSON(data []byte)
メソッドを書けばよい。
type Event struct {
// もろもろのJSON構造の定義
// 構造体にマッピングする前のJSONを保存する場所
Raw json.RawMessage `json:"-"`
}
func (e *Event) UnmarshalJSON(data []byte) error {
type event Event
var ee event
err := json.Unmarshal(data, &ee)
if err != nil {
return err
}
*e = (Event)(ee)
e.Raw = data
return nil
}
やっていることは単純でアンマーシャルしたあと元データをjson.RawMessage
フィールドで参照しているだけだ。
ただ型を定義しなおしてからアンマーシャルしているのがポイントとなる。
type event Event
var ee event
err := json.Unmarshal(data, &ee)
Defind Type
で名前を変えると独自定義したUnmarshalJSON(data []byte)
メソッドを呼ばなくなる
型を定義しなおす理由は2つある。
最初の理由は「型を定義しなおさないといけない理由」で、同じ型のままjson.Unmarshal
関数を使うと(e *Event) UnmarshalJSON(data []byte)
が再帰的に呼び出されるだけになってしまうからだ。
別定義したevent
型には(e *event) UnmarshalJSON(data []byte)
メソッドは定義されていない。そのため、通常のアンマーシャルが行われるだけになる。
もうひとつの理由は「型を定義しなおしたほうがよい理由」で、このように定義されたevent
型はEvent
型とまったく同じ構造体定義をもつ。
そのため、(Event)(ee)
のようなキャストも必ず成功する。また、Event
型の構造体定義に変更が入ったときはevent
型の定義も当然更新される。
メンテフリーでEvent
型の内容にevent
型が追従するため、「いつの間にか壊れていた」ということも発生しない。
終わりに
今回のやりかたはstripe-go
を参考にしている。
もっというと@vvakameさんのツイートで見かけたから把握していたという背景もある。
おぉー Stripeのライブラリで面白いテクニック見つけた… type hoge Hoge; var v hoge; json.Unmarshal(b, &v) とかしてUnmarshalJSONを適用しない的なやつ
— わかめ@毎日猫がいる (@vvakame) September 20, 2020
OSSのコードを読むのは大事。