Go言語におけるJSONのDecodeの注意点・対策

ここ数ヶ月、APIのレスポンスをParseする処理をGo言語で書く機会がたくさんありました。
APIから返ってくるJSONの形式によっては、`json.Unmarshal` ではどうにも太刀打ちできないことが何度かありました。

Go言語でJSONをParseする際に注意すべきこと、使える手段についてまとめておきます

# 何はともあれコレ！ `json.Unmarshal`
- `func Unmarshal(data []byte, v interface{}) error`
- 標準ライブラリ `json` のメソッド。 
    - json形式のbyte列を受け取り、Parseして `v` が指す値に格納する。第二引数 `v` はpointerでなければならない。

```golang:サンプルコード
import (
	"encoding/json"
	"fmt"
)

func main() {
	var jsonBlob = []byte(`[
	{"Name": "Platypus", "Order": "Monotremata"},
	{"Name": "Quoll",    "Order": "Dasyuromorphia"}
]`)
	type Animal struct {
		Name  string
		Order string
	}
	var animals []Animal
	err := json.Unmarshal(jsonBlob, &animals) // <- 第二引数はポインタ。メソッド内で代入は完了する
	if err != nil {
		fmt.Println("error:", err)
	}
	fmt.Printf("%+v", animals) // => [{Name:Platypus Order:Monotremata} {Name:Quoll Order:Dasyuromorphia}]
}
```

- 関数の名前について
    - コンピュータサイエンスにおいて、marshallingとは、「送信に適したデータ形式(この場合はjson)に変換する」という意味を持つ。
        - シリアライゼーション(serialization)とほぼ同義。
    - この逆操作(シリアライズされたオブジェクトを内部データ構造に変換すること）をUnmarshallingと呼ぶ。

- マッピングルール
    - jsonUnmarshalは、渡されたjsonのKey名と一致するStructのFieldに値を入れる。この時、大文字・小文字が一致していなくても構わない。

## 参考
- [公式ドキュメント json.Unmarshal](https://pkg.go.dev/encoding/json#Unmarshal)
- [Marshalの本当の意味知ってる？](https://qiita.com/silver_11/items/bbd776c0a063a13ffd30)

# ケース1: ケバブケースのフィールド名
## 問題
[Ubiregi API](https://ubiregiinc.github.io/ubiregi-api/)というAPIのクローラーを開発している中で、こんなレスポンスに出会いました。
```json:ubiregiのresponse
{
  "timestamp": "2021-09-12T13:40:15Z",
  "next-url": "https://ubiregi.com/api/3/xxxx"
}
```
`next-url` というフィールド名は曲者です。Go言語ではStructのフィールド名に`-`（ハイフン）を使えません。これはどうやってマッピングしたらいいでしょうか？

## 回答:構造体タグを用いたマッピングのカスタマイズ
こんな時、`json.Unmarshal`は**構造体タグ**によって動作をカスタマイズできます。
- 構造体タグとは
    - 構造体(Struct）にメタ情報を付与できる記法
    - 型情報を扱う標準ライブラリ`reflect`の機能を使うことで取得できる

```go:構造体タグの例
type Organization struct {
    Name     string `json:"name" validate:"required"`
    Language string `json:"language,omitempty" validate:"required"`
    CreatedAt time.Time `json:"-"`
}
```

- 標準ライブラリ `json`で使える構造体タグはこのページがわかりやすいです。
    - [Golangで構造体を使ったJSON操作で出来ることを調べてみた](https://dev.classmethod.jp/articles/struct-json/)

さて、上記のUbiregi APIのレスポンスをParseする際は、Structを以下のように指定すればOKです。
```golang
type CheckoutsResponse struct {
	Timestamp time.Time `json:"timestamp"`
	NextURL   string    `json:"next-url"`
}
```

あとは普通にUnmarshalするだけ。
```go
var output CheckoutsResponse
err := json.Unmarshal(responseBody, &output)
```

## 参考記事
- [Goの構造体にメタ情報を付与するタグの基本](https://qiita.com/itkr/items/9b4e8d8c6d574137443c)

# ケース2: RFC3339以外のフォーマットの時刻情報をParseする

## 問題
[Twitter Premium API v1.1 Search API](https://developer.twitter.com/en/docs/twitter-api/premium/search-api/api-reference/premium-search)というAPIのクローラーを開発している中で、こんなレスポンスに出会いました。

```
{
	"id": 123456789,
	"created_at": "Thu Oct 07 12:14:13 +0000 2021",
	...
}
```

json.Unmarshalは、RFC3339(`2006-01-02T15:04:05Z07:00`)の形式であれば問題なくParseできますが、それ以外の形式のデータはそのままではフォーマットできません。
この `created_at` は RubyDate(`Mon Jan 02 15:04:05 -0700 2006`)という形式なので、一手間必要です。どうすればいいでしょうか？

（時刻のフォーマットって何？という方は、[Go 標準ライブラリ time](https://pkg.go.dev/time)の記事を読んでください）

## 回答
- 独自型を作成し、`UnmarshalJSON` というメソッドを定義することで解消できます。
- [Go で時刻を json.Unmarshal する際の注意点](https://kenzo0107.github.io/2020/05/19/2020-05-20-go-json-time/)の解説がわかりやすいので、こちらに内容を譲ります。
- 他にも `DecodeHook` を使う手もありますが、ここでは省略します。

# ケース3: データ形式が変わる
## 問題
同じく、[Twitter Premium API v1.1 Search API](https://developer.twitter.com/en/docs/twitter-api/premium/search-api/api-reference/premium-search)というAPIのクローラーを開発している中でGeoデータ(地理データ)をParseする機会がありました。

https://developer.twitter.com/en/docs/twitter-api/enterprise/enrichments/overview/profile-geo

geoデータは `Point`(=点） と `Polygon`(=面積)の2パターンがあり、それぞれ形式が異なります。

```json:point
"geo": {
    "coordinates": [
        135.75385,
        35.02107
    ],
    "type": "Point"
}
```

```json:Polygon
"geo": {
    "coordinates": [
        [
            [
                -74.026675,
                40.683935
            ],
            [
                -74.026675,
                40.877483
            ],
            [
                -73.910408,
                40.877483
            ],
            [
                -73.910408,
                40.3935
            ]
        ]
    ],
    "type": "Polygon"
}
```

`Point`の場合、`coordinates`は `[2]float64` になりますが、`Polygon`の場合は `[][][2]float64`となります。
つまり、**`geo`のデータ形式は、ツイートによって異なる**ということです。
この場合、どのように書いたらいいのでしょうか。

## 回答
このようなケースには、`mapstructure` というライブラリを使うと対応できます。

mapstructureは、まずbyte列を`map[string]interface{}`に変換し、次に`map[string]interface{}`を指定のStructに変換する、という二段階の変換を必要とします。なぜかについては、[公式ドキュメント](https://pkg.go.dev/github.com/mitchellh/mapstructure#section-readme)のBut Why?の部分を読んでください。

細かい使い方は[mapstructureを使って複雑な構造のJSONを構造体にマッピングする](https://qiita.com/syumai/items/9e2441b0e8fda8281fdf)がわかりやすいのでこちらを参照のこと。

# まとめ
- Go言語でjsonのDecodeを行うときは、通常は標準ライブラリの `json.Unmarshal` メソッドを利用する
- `json.Unmarshal` は、渡されたjsonのKey名と一致するStructのFieldに値を入れるが、Key名とField名が一致させられない場合は構造体タグの利用を検討する
- `json.Unmarshal` で時刻を扱う場合、`json.Unmarshal` は決め打ちで RFC3339(ISO8601)のフォーマットとしてDecodeを行う。他の形式の時刻データを扱う場合は、独自型を作成し、`UnmarshalJSON` というメソッドを定義することで対応できる。（他の手もある）
- 場合によって型が変わる場合は、 `mapstructure` などのライブラリを使うことで対応できる。

# ほか、オススメの記事
Go言語におけるJsonのParseに困ったら、以下の記事が参考になると思います。
- [Goにおけるjsonの扱い方を整理・考察してみた ~ データスキーマを添えて](https://zenn.dev/hsaki/articles/go-convert-json-struct)
- [The Go Blog  JSON and Go](https://go.dev/blog/json)