12
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HRBrainAdvent Calendar 2024

Day 9

interface 型フィールドを持つ構造体に unmarshal する方法

Last updated at Posted at 2024-12-08

はじめに

プロダクトとして開発しているサーベイ機能において,サーベイの設問内容をカスタマイズできるようにする開発タスクに取り組んでいました.カスタマイズした設問内容はデータベースに保存して,必要に応じて取り出す必要がありました.この際,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 をカスタマイズしましょう!

12
5
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
12
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?