こんばんは、しーまんです。
Golangでデータを扱う際は構造体を定義して扱うのが一般的です。HTTPリクエストのレスポンスデータに対しても構造体を定義し受け取ります。
その際の構造体を作成するwebサービスとして JSON-to-Go はよくお世話になっております。
しかし、外部サービス連携などでapiをリクエストした際に構造体を定義せずにjson出力したい場合などがあります。そのような場合どのように実装すればよいのか検証したので、その方法を解説致します。
構造体を定義しない理由
構造体を定義しない状況っていうのはかなり特殊な状態だと思いますので、私の事例を元に解説致します。
最近クラウドサービスの普及により、REST api で情報を取得できるSaaSが増えてきています。
そんな中でapiで取得したデータをjsonに吐き出し、そのデータをBigQueryへ投入したい要件がありました。
イメージとしては下図のような感じですね。
2つの構造データ作成を省略
上記の実装の際、BigQueryにデータを投入する時にAutodetectという設定があり、これはテーブルのカラム定義を自動で推論してくれる機能です。Autodetect機能を使用しない場合ではBigQuery側のテーブルの構造データも作成する必要があります。
つまり、Golang側では
APIの取得に構造体を作成し、BigQueryのデータ投入にテーブルの構造データ作成
という2つほど、データの構造を定義しないといけません。
それはとても面倒で工数もかかりますし、APIの数も大量にありますので、構造の定義を省きたいというわけです。
BigQueryに関しては上記で取り上げたAutodetect機能を使用すれば解決しますが、一応制限事項もありますので、使用する際はマニュアルを参照ください。
SaaSのAPI取得で構造体定義したくない理由
またSaaSのAPIに関して構造体定義したくない理由が3点ほどあります。
- SaaS側で提供されているマニュアル、テストレスポンスに誤りがある場合が存在する
実際に本番リクエストを送って確認するまで安心できません。 - レスポンスが頻繁に更新される
レスポンスの更新が行われるともちろん構造体の更新も必要になります。 - 必須値以外が漏れる可能性がある
返って来ない場合があるレスポンスの値は構造体定義時に漏れる可能性があります。
上記のような理由からSaaSのAPI取得は可能であれば構造体を定義しないで、動的に扱いたいと思います。
HTTPレスポンスを動的に扱う方法
では実際にHTTPレスポンスを動的に扱う方法をみていきましょう。
まずは構造体を定義する通常パターン
エラーを全て無視して簡易的に書くとこのようなコードになります。
package main
import (
"encoding/json"
"io"
"net/http"
"os"
)
func main() {
url := "https://xxxx"
req, _ := http.NewRequest(http.MethodGet, url, nil)
client := new(http.Client)
resp, _ := client.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
// JSONを構造体にエンコード
var response Response
json.Unmarshal(body, &response)
file, _ := os.Create("response.json")
defer file.Close()
json.NewEncoder(file).Encode(response)
}
type Response struct {
Column []struct {
ID int `json:"ID"`
Name string `json:"NAME"`
} `json:"Column"`
}
上記コードで json.Unmarshal のところで、構造体を渡しています。
しかし、json.Unmarshalの第2引数には *interface{} を渡すことが可能です。また json.EncoderのEncodeも interface{} を渡すことが可能です。
こちらを利用して動的にレスポンスを扱うことが可能になります。
実際に動的にレスポンスを取得してJSONを出力するコードをみていきましょう。
package main
import (
"encoding/json"
"io"
"net/http"
"os"
)
func main() {
url := "https://xxxx"
req, _ := http.NewRequest(http.MethodGet, url, nil)
client := new(http.Client)
resp, _ := client.Do(req)
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
// JSONを構造体にエンコード
var response interface{}
json.Unmarshal(body, &response)
file, _ := os.Create("response.json")
defer file.Close()
json.NewEncoder(file).Encode(response)
}
変更点は下記ですね。
var response Response が var response interface{} に変わっています。
そうです、interfaceをうまく使用してあげると構造体を定義しなくてもレスポンスをjsonに吐き出すことが可能になります。
ただし、今回はjsonを吐き出すことが目的でしたので、こちらの方法を取りましたが、実際に値を使うことを考えるととても大変です。
下記のようにキャストの嵐で値をとりだすことになりコード量が半端じゃなく増えます。
response.(map[string]interface{})["Column"].([]interface{})[0].(map[string]interface{})["NAME"].(string)
また、そもそも使用する値が決まっているのだったら構造体も定義できますよね。
ですので、今回の要件のように特殊なケースを除いては、ちゃんと構造体を定義した方が処理を楽に書くことができます。
まとめ
今回はHTTPリクエストのレスポンスを動的にJSON化する方法を解説しました。
なかなかニッチなケースではありますが、元々PHPerである私としては構造体定義なしでもっと気軽にレスポンスを扱いたいって思いもあります。まぁ余計なreflectionなどの処理がなくなるので処理自体は高速なんでしょうね。
ということで、このようなパターンで困っている方がどれだけいるかは分かりませんが、同じように困っている方の参考になれば幸いです。
コメント