配列が 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 をいじったら,各子クライアントに設定するのを忘れないように.