配列が null だと Azure サーバがエラーを返す問題
Posted: , Modified: Go Azure Swagger Qiita
本稿は Qiita 投稿記事 のバックアップです.
概要
Go から Microsoft Azure を利用するの方法でソースコードを生成すると, スライス型の構造体メンバに omitempty が付かず API 呼び出しがエラーになる場合があった. その原因ととりあえずの対策をまとめる.
go-swagger が生成する構造体
例えば,
$ swagger generate client \
-f https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/batch/2016-07-01.3.1/swagger/BatchService.json \
-t batch
上記のコマンドで Batch API のバインディングを作った場合, ジョブプールの追加に必要なパラメータを表す構造体として,下記のような PoolAddParameter が生成される.
type PoolAddParameter struct {
// The list of application packages to be installed on each compute node in the pool.
//
// This property is currently not supported on pools created using
// the virtualMachineConfiguration (IaaS) property.
ApplicationPackageReferences []*ApplicationPackageReference `json:"applicationPackageReferences"`
AutoScaleEvaluationInterval strfmt.Duration `json:"autoScaleEvaluationInterval,omitempty"`
AutoScaleFormula string `json:"autoScaleFormula,omitempty"`
// 以下略
}
ここで,ApplicationPackageReferences のタグに omitempty が付いていないため, この構造体インスタンスを Marshal すると,ApplicationPackageReferences はデフォルトで null が書き出されてしまう.
Azure は null が含まれているリクエストを無条件にリジェクトするので, null が書き出されないように要素数 0 のスライスを設定する必要がある.
たいていの場合は,この空のスライスを設定しておけば問題ないのだが,上記の PoolAddParameter は注釈に virtualMachineConfiguration (IaaS) property とは競合すると書かれていて, 実際 virtualMachineConfiguration に値が設定してあると,空のスライスであってもエラーになる. (どちらか片方だけ設定するように言われる)
go-swagger 的には未設定と空を区別するために omitempty をあえて付けないでいるようなので, (参考: can’t distinguish between an empty array and null array for non-required array fields) Azure サーバの方で null を認めるかプログラムの方でなんとかするしかない.
とりあえずの対策
自動生成されたソースコードを調べて omitempty をつけて回るか, リフレクションや ast を使って動的に対処すれば良いのだが, OAuthアクセストークンを使ってMicrosoft Azureにアクセスするで扱ったクライアントの Transport に, リクエストの構造体をテキストメッセージ(この場合は JSON 文書)に変換する Producer を登録できるので, この Producer を書き換えて対応することにする.
Producer は,go-openapi/runtime パッケージにて,
type Producer interface {
// Produce writes to the http response
Produce(io.Writer, interface{}) error
}
と定義されている.
今回は,とりあえず動けば良いと思って下記のような Producer を用意した.
type MinimalJSONProducer struct {
regexp *regexp.Regexp
blank []byte
}
func NewMinimalJSONProducer() *MinimalJSONProducer {
return &MinimalJSONProducer{
regexp: regexp.MustCompile("(\"[^\"]+?\":null,?|,\"[^\"]+\":null)"),
blank: []byte(""),
}
}
func (p *MinimalJSONProducer) Produce(out io.Writer, msg interface{}) (err error) {
data, err := json.Marshal(msg)
if err != nil {
return
}
data = p.regexp.ReplaceAllLiteral(data, p.blank)
_, err = out.Write(data)
return
}
Producer の登録は,OAuthアクセストークンを使ってMicrosoft Azureにアクセスするの時と同様にクライアントを
*httptransport.Runtime
にキャストして登録する.
API 呼び出しの通信で使われているリクエストのコンテンツタイプは application/json; odata=minimalmetadata
なので,
このコンテンツタイプ用の Producer として先ほど定義した MinimalJSONProducer を与える.
import(
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
// go-swagger が生成したパッケージ
"github.com/jkawamoto/roadie/cloud/azure/batch/client"
)
cli := client.NewHTTPClient(strfmt.NewFormats())
switch transport := cli.Transport.(type) {
case *httptransport.Runtime:
transport.Producers["application/json; odata=minimalmetadata"] = NewMinimalJSONProducer()
// 子クライアントに登録
cli.Accounts.SetTransport(transport)
cli.Jobs.SetTransport(transport)
}
Transport をいじったら,各子クライアントに設定するのを忘れないように.