非推奨なJSONの配列型をUnmarshalする

Unmarshalシリーズ3回目?となりましたが今回非推奨なJSONの配列型のUnmarshal方法を扱います。

通常JSONの配列は以下に示すような書き方が一般的ですが、

{
  "list": {
    "listItem": [
      {
        "foo": "1",
        "bar": "2",
        "buz": "3"
      },
      {
        "foo": "4",
        "bar": "5",
        "buz": "6"
      },
      {
        "foo": "7",
        "bar": "8",
        "buz": "9"
      }
    ]
  }
}

今回の記事で非推奨としている配列は以下に示すような書き方をしているものです。

  {
    "list": {
      "listItem": {
        "foo": "1",
        "bar": "2",
        "buz": "3"
      },
      "listItem": {
        "foo": "4",
        "bar": "5",
        "buz": "6"
      },
      "listItem": {
        "foo": "7",
        "bar": "8",
        "buz": "9"
      }
    }
  }

このJSONをUnmarshalする場合以下のような構造体を定義した上で処理を実行することになると思いますが、

type S struct {
    List List `json:"list"`
}

type List struct {
    ListItem []Item `json:"listItem"`
}

type Item struct {
    Foo string `json:"foo"`
    Bar string `json:"bar"`
    Buz string `json:"buz"`
}

しかし実際に実行してみると以下のようなエラーが出てUnmarshal自体行うことができません。

json: cannot unmarshal object into Go struct field List.list.listItem of type []main.Item
{List:{ListItem:[]}}

実行コード: https://go.dev/play/p/3AImwYI8ejs

では、このケースの場合どのようにアプローチすればいいかというと、List構造体のUnmarshalJSONメソッド内でlistItemをユニークなKeyに置換することでmap[string]interface{}に一旦Unmarshalし、そうすることでlistItemをfor ... rangeでループ処理できるようになります。

そしてループ処理内ではreflectionにより各フィールドの値を取り出してItem構造体に詰めていくという愚直なパース処理を行うことで無事Unmarshalすることができました。

func (l *List) UnmarshalJSON(b []byte) error {
    // 同一KeyのオブジェクトはUnmarshal時に上書きされてしまうのでKeyをユニークな値に置換する
    k := "listItem"
    c := strings.Count(string(b), k)
    rep := strings.Replace(string(b), k, "replaced00", 1)
    for i := 0; i < c; i++ {
        rep = strings.Replace(string(rep), k, fmt.Sprintf("replaced%02d", i+1), 1)
    }

    var itf map[string]interface{}
    if err := json.Unmarshal([]byte(rep), &itf); err != nil {
        return err
    }
    // => map[replaced00:map[bar:2 buz:3 foo:1] replaced01:map[bar:5 buz:6 foo:4] replaced02:map[bar:8 buz:9 foo:7]]

    var items []Item
    for _, v := range itf {
        rv := reflect.ValueOf(v)
        foo := rv.MapIndex(reflect.ValueOf("foo"))
        bar := rv.MapIndex(reflect.ValueOf("bar"))
        buz := rv.MapIndex(reflect.ValueOf("buz"))
        item := Item{
            Foo: fmt.Sprintf("%v", foo),
            Bar: fmt.Sprintf("%v", bar),
            Buz: fmt.Sprintf("%v", buz),
        }
        items = append(items, item)
    }
    l.ListItem = items

    return nil
}
// 実行結果
{List:{ListItem:[{Foo:1 Bar:2 Buz:3} {Foo:4 Bar:5 Buz:6} {Foo:7 Bar:8 Buz:9}]}}

実行コード: https://go.dev/play/p/LxzmjUUMf-y

もしこれよりスマートな方法があれば教えて下さい🙇‍♂️