はじめに
プロダクトとして開発しているサーベイ機能において,サーベイの設問内容をカスタマイズできるようにする開発タスクに取り組んでいました.カスタマイズした設問内容はデータベースに保存して,必要に応じて取り出す必要がありました.この際,interface のフィールドを持つ Go の構造体にデータを変換するのに一工夫が必要だったので,それを紹介します.
interface 型フィールドを持つ構造体をそのまま unmarshal してみる
まず,今回扱った interface 型フィールドを持つ構造体の定義は以下のようなものになります.Question 構造体の Option フィールドは OptionInterface を満たすものを想定している作りになっています.
type Question struct {
Id int
Content string
Option OptionInterface
}
type OptionInterface interface {
firstMethod()
secondMethod()
}
ここで,OptionInterface を満たすような SomeOption 構造体を定義すると以下のようになります.
type SomeOption struct {
OptionInt int
OptionStr string
}
func (o SomeOption) firstMethod {}
func (o SomeOption) secondMethod {}
Question 構造体インスタンスを作成し,json としてデータベースへの保存と読み出しを想定するため,一度 marshal し,そのまま unmarshal してみます.
func main() {
// Question 構造体インスタンス
question := Question{
Id: 1,
Content: "question",
Option: SomeOption{
OptionInt: 2,
OptionStr: "option",
},
}
// json データとしてデータベースへの保存を想定
v, _ := json.Marshal(question)
// json データをデータベースから取り出しを想定
var q Question
err = json.Unmarshal(v, &q)
if err != nil {
fmt.Println(fmt.Errorf("%w", err))
}
}
すると,json: cannot unmarshal object into Go struct field Question.Option of type main.OptionInterface
というエラーが返ってくることがわかります.実際に unmarshal した先の q の値を見てみると,{1 question <nil>}
で,Option フィールドが nil でうまく読み出せていないです.(コード再現)
Unmarshaler を実装して再度 unmarshal してみる
json パッケージが提供する Unmarshal 関数の説明には,Unmarshaler が実装されていればそれを呼ぶ旨が書いてあります.
To unmarshal JSON into a value implementing Unmarshaler, Unmarshal calls that value's [Unmarshaler.UnmarshalJSON] method [...].
Question 構造体の Option フィールドをうまく unmarshal するために,以下のような Unmarshaler の実装を加えます.
func (q *Question) UnmarshalJSON(b []byte) error {
// unmarshal できている値はそのまま利用する
temp := struct {
Id int
Content string
}{}
if err := json.Unmarshal(b, &temp); err != nil {
return err
}
q.Id = temp.Id
q.Content = temp.Content
// 全てのフィールドを json.RawMessage として unmarshal
// json.RawMessage は生 JSON 値
raw := map[string]json.RawMessage{}
if err := json.Unmarshal(b, &raw); err != nil {
return err
}
// フィールド名から unmarshal 方法を切り替える
for k, v := range raw {
switch k {
case "Option":
option, _ := json.Marshal(v)
// Option の中にあるデータを SomeOption 構造体に割り当てる
var o SomeOption
err := json.Unmarshal(option, &o)
if err != nil {
return err
}
q.Option = o
}
}
return nil
}
すると,unmarshal した先の Question 構造体インスタンス q の値は,{1 question {2 option}}
と Option フィールドが nil ではなく,想定していた値が返ってくるようになります.(コード再現)
Unmarshaler 実装の留意点
実装した UnmarshalJSON 関数をみると,システム保守の観点で少し不安な点が残ります.
- unmarshal 方法を切り替える際に,対象のフィールド名を生の文字列で分岐させている箇所(
case "Option":
の部分)で,フィールド名の変更に頑健ではない点 - データを割り当てる先の構造体の構成によっては,より処理が複雑になってしまう点(今回扱った SomeOption 構造体はシンプル)
他にもあるかもしれませんが,上記の点などは実際に利用する場合に留意する必要があると考えます.
まとめ
interface 型フィールドを持つ構造体に対して,Unmarshaler を実装することで,データを unmarshal する方法を紹介しました.実利用では留意する点があるかもしれませんが,必要に応じて unmarshal をカスタマイズしましょう!