ここ数ヶ月、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でなければならない。
- json形式のbyte列を受け取り、Parseして
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と呼ぶ。
- コンピュータサイエンスにおいて、marshallingとは、「送信に適したデータ形式(この場合はjson)に変換する」という意味を持つ。
-
マッピングルール
- jsonUnmarshalは、渡されたjsonのKey名と一致するStructのFieldに値を入れる。この時、大文字・小文字が一致していなくても構わない。
参考
ケース1: ケバブケースのフィールド名
問題
Ubiregi APIというAPIのクローラーを開発している中で、こんなレスポンスに出会いました。
{
"timestamp": "2021-09-12T13:40:15Z",
"next-url": "https://ubiregi.com/api/3/xxxx"
}
next-url
というフィールド名は曲者です。Go言語ではStructのフィールド名に-
(ハイフン)を使えません。これはどうやってマッピングしたらいいでしょうか?
回答:構造体タグを用いたマッピングのカスタマイズ
こんな時、json.Unmarshal
は構造体タグによって動作をカスタマイズできます。
- 構造体タグとは
- 構造体(Struct)にメタ情報を付与できる記法
- 型情報を扱う標準ライブラリ
reflect
の機能を使うことで取得できる
type Organization struct {
Name string `json:"name" validate:"required"`
Language string `json:"language,omitempty" validate:"required"`
CreatedAt time.Time `json:"-"`
}
- 標準ライブラリ
json
で使える構造体タグはこのページがわかりやすいです。
さて、上記のUbiregi APIのレスポンスをParseする際は、Structを以下のように指定すればOKです。
type CheckoutsResponse struct {
Timestamp time.Time `json:"timestamp"`
NextURL string `json:"next-url"`
}
あとは普通にUnmarshalするだけ。
var output CheckoutsResponse
err := json.Unmarshal(responseBody, &output)
参考記事
ケース2: RFC3339以外のフォーマットの時刻情報をParseする
問題
Twitter Premium API v1.1 Search APIという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の記事を読んでください)
回答
- 独自型を作成し、
UnmarshalJSON
というメソッドを定義することで解消できます。 - Go で時刻を json.Unmarshal する際の注意点の解説がわかりやすいので、こちらに内容を譲ります。
- 他にも
DecodeHook
を使う手もありますが、ここでは省略します。
ケース3: データ形式が変わる
問題
同じく、Twitter Premium API v1.1 Search APIというAPIのクローラーを開発している中でGeoデータ(地理データ)をParseする機会がありました。
geoデータは Point
(=点) と Polygon
(=面積)の2パターンがあり、それぞれ形式が異なります。
"geo": {
"coordinates": [
135.75385,
35.02107
],
"type": "Point"
}
"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に変換する、という二段階の変換を必要とします。なぜかについては、公式ドキュメントのBut Why?の部分を読んでください。
細かい使い方はmapstructureを使って複雑な構造のJSONを構造体にマッピングするがわかりやすいのでこちらを参照のこと。
まとめ
- 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に困ったら、以下の記事が参考になると思います。