OpenAPI定義に沿ってバリデーションをしてくれるGoのライブラリを書いた

GitHub - aereal/go-openapi3-validation-middleware: net/http middleware to validate HTTP requests/responses against OpenAPI 3 schema using kin-openapi.

kin-openapiというOpenAPI 3定義を読んでリクエスト・レスポンスのバリデーションをしてくれるGoのライブラリがあるんだけど微妙に使い勝手が悪い。 素朴に使おうとするとHTTPハンドラ内でバリデーションに関するコードを書く必要があって関心を分離させるという目的を果たすにはちょっと弱いし、得られたエラーを一貫して取り扱うにはエラーをHTTPレスポンスに加工して返すところまで一気通貫で取り扱いたい。

Goでnet/httpを使ってHTTPサーバを書く時は、RackやPlackのように、ミドルウェアあるいはサンク (thunk) を組み合わせてリクエスト/レスポンスの参照や加工を行えるので、この仕組みに乗りたい。

というわけでREADMEのsynopsisから引用:

import (
    "net/http"

    "github.com/aereal/go-openapi3-validation-middleware"
    "github.com/getkin/kin-openapi/routers"
)

func main() {
    var router routers.Router // must be built with certain way
    mw := openapi3middleware.WithValidation(openapi3middleware.MiddlewareOptions{Router: router})
    http.Handle("/", mw(http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
        // this handler is called if validation succeeds
    })))
}

見てわかるように、http.Handlerを扱えるライブラリならnet/http以外でも使える。 実際に自分はhttptreemuxで使った。

WithValidation はリクエストとレスポンスどちらも検証する。 用途に合わせてリクエストのみを検証する WithRequestValidation, レスポンスのみを検証する WithResponseValidation というエントリポイントをそれぞれ用意してある。

  • 自動テストやQAが十分であればレスポンスの検証は本番環境では不要という考え方もありうること
  • 実装の都合上、書き出されたレスポンスボディをすべてメモリ上に確保するためGoのio.Writerインターフェースの良さをスポイルしてしまっていること

……といった理由からレスポンスは検証せずリクエストのみ検証するというオプションを用意した。 ちなみに WithValidationWithRequestValidationWithResponseValidation を合成しただけ。ちょっとおしゃれで好き。

実装してのおもしろポイントといえばhttp.ResponseWriterを独自実装したところとか。

go-openapi3-validation-middleware/response_writer.go at main · aereal/go-openapi3-validation-middleware · GitHub

http.ResponseWriterはインターフェースなのでnet/httpがデフォルトで持っている実装以外を使うこともできる。

このライブラリではレスポンスボディとステータスコードを保持するための実装を書いた。 レスポンスを検証する際の最終的なレスポンスは、内側のHTTPハンドラが書き出すかもしれないし、このライブラリがエラー報告を書き出すかもしれない。 なのでbufferingResponseWriterのWrite()とWriteHeader()の書き込みインターフェースはそれらを呼び出した時点では引数を保持するだけにしてある。

レスポンスを書き換えるパターンのミドルウェアは初めて書いたので手札が増えてよかった。