一部可変的な構造体の要素を含むJSONをUnmarshalして可変部の値を取り出す

背景

  • 以下に示すような一部データ構造が可変的な要素を含むJSONをUnmarshalしてかつその要素の値を取り出したい
  • 前提として可変部のタイプはJSONに含まれるある値(ここではkeyとする)で判定可能とする
  • 今回"variable"の要素が[]stringまたは[]structの場合を扱う
{
  "list": [
    {
      "key": "A",
      "value": "aaa"
    },
    {
      "key": "B",
      "variable": [
        "b-1",
        "b-2"
      ]
    },
    {
      "key": "C",
      "variable": [
        {
          "foo": "1",
          "bar": "2"
        },
        {
          "foo": "3",
          "bar": "4"
        }
      ]
    }
  ]
}

Unmarshalするための構造体の定義

type Input struct {
    List []Element `json:"list"`
}

type Element struct {
    Key          string      `json:"key"`
    Value        string      `json:"value,omitempty"`
    VariablePart interface{} `json:"variable,omitempty"`
}

type ObjectVariable struct {
    Foo string `json:"foo"`
    Bar string `json:"bar"`
}

"variable"に対応するフィールド(VariablePart)はinterface{}型とし定義する。 今回の場合ここをjson.RawMessageにしていないのは後で明示的に構造体を割り当てるため。

UnmarshalJSONの実装

func (e *Element) UnmarshalJSON(b []byte) error {
    type Alias Element
    a := &struct {
        // ここでは一旦json.RawMessageにしておく
        VariablePart json.RawMessage `json:"variable"`
        *Alias
    }{
        Alias: (*Alias)(e),
    }

    // 一旦全体をUnmarshal
    if err := json.Unmarshal(b, &a); err != nil {
        return err
    }

    // Keyに応じてVariablePartのUnmarshalする構造体を判定して明示的に割り当てる
    switch e.Key {
    case "B":
        var s []string
        if err := json.Unmarshal(a.VariablePart, &s); err != nil {
            log.Fatal(err)
        }
        e.VariablePart = &s
    case "C":
        var s []ObjectVariable
        if err := json.Unmarshal(a.VariablePart, &s); err != nil {
            log.Fatal(err)
        }
        e.VariablePart = &s
    default:
        return nil
    }

    return nil
}

const input = `最初に書いたjson`

func main() {
    var i Input
    if err := json.Unmarshal([]byte(input), &i); err != nil {
        log.Fatal(err)
    }
}

UnmarshalJSON()は関連付けた構造体がUnmarshalされる際に呼び出されるメソッドで、今回の場合はElement構造体がUnmarshalされる際に呼び出され、keyに応じてUnmarshalする構造体を判定してUnmarshalしElement.VariablePartに割り当てています

可変部の値を取り出す

// 原則としてBはjson内に1つしか含まれない
func (i *Input) GetVariableB() []string {
    as := []string{}
    for _, e := range i.List {
        if e.Key != "B" {
            continue
        }
        switch e.VariablePart.(type) {
        case *[]string:
            as = *e.VariablePart.(*[]string)
        }
        break
    }
    return as
}

// 原則としてCはjson内に1つしか含まれない
func (i *Input) GetVariableC() []ObjectVariable {
    ov := []ObjectVariable{}
    for _, e := range i.List {
        if e.Key != "C" {
            continue
        }
        switch e.VariablePart.(type) {
        case *[]ObjectVariable:
            ov = *e.VariablePart.(*[]ObjectVariable)
        }
        break
    }
    return ov
}

やり方は色々あると思いますが、今回はくどいですが念の為keyの値を確認しつつ型もチェックした上で要素を返すメソッドを実装しました

func main() {
...
    // Unmarshalした可変部の値を取得
    ss := i.GetVariableB()
    for _, s := range ss {
        log.Printf("%q", s)
    }

    lo := i.GetVariableC()
    for _, o := range lo {
        log.Printf("%+v", o)
    }
}
$ go run main.go
"b-1"
"b-2"
{Foo:1 Bar:2}
{Foo:3 Bar:4}

まとめ

  • 今回可変部のあるJSONに対してその可変部の値を取り出したかった
  • 可変部に応じた構造体を明示的にUnmarshalするためElement構造体用のUnmarshalJSON()に処理を実装した
  • さらにその可変部の値を取り出す専用のメソッドを定義することで値を取り出せるようにしてみた

参考