いつもoapi-codegenでopenapi.yamlからGoのコードを生成するようにしている。 oapi-codegenの使い方については下記参照

OpenAPIでGoとTypeScriptのコード生成

OpenAPI仕様書からGoの構造体を作る

OpenAPIでパラメータに制約をつける

OpenAPI Documentでは、JSON Schema の定義に従って schema に制約を書くことができる。 OpenAPI Specification - Version 3.0.3 | Swagger draft-wright-json-schema-validation-00

次のように patternformatmaxLength などが定義できる。

openapi.yaml

paths:
  /hello:
    get:
      summary: Hello
      operationId: hello
      parameters:
        - name: id
          in: query
          description: user id
          required: true
          schema:
            type: string
            pattern: "^[0-9A-F]+$"
        - name: updated
          in: query
          schema:
            type: string
            format: date-time

oapi-codegenを使えば、 schema に書いた制約をもとにリクエストのバリデーションを行うことができる。

以下はEchoの場合

package main
 
import (
	"my-application/oapigen"
 
	oapiMiddleware "github.com/deepmap/oapi-codegen/pkg/middleware"
	"github.com/getkin/kin-openapi/openapi3"
	"github.com/labstack/echo/v4"
)
 
func main() {
	e := echo.New()
 
	// oapi-codegenで生成したコードにあるGetSwagger()でopenapi specを取得
	swagger, err := oapigen.GetSwagger()
	if err != nil {
		panic(err)
	}
	// ホストの検証が必要でなければnilにしておく
	swagger.Servers = nil
	// middlewareにValidatorをセットすることでリクエストの検証が行われる
	e.Use(oapiMiddleware.OapiRequestValidator(swagger))
 
	// 独自のformatを定義したい場合はこのようにする
	openapi3.DefineStringFormat("custom-date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}$`)
 
 
}
 

OpenAPIスキーマ駆動開発におけるoapi-codegenを用いたリクエストバリデーション - HRBrain Blog こちらに書いてあるとおり、一通りのバリデーションはできるようになっている

middlewareのソースはこちら https://github.com/deepmap/oapi-codegen/blob/ab90f1927bc5ec3e29af216d4298fbb4780ae36d/pkg/middleware/oapi_validate.go#L58

別ライブラリと組み合わせる

https://github.com/go-ozzo/ozzo-validationhttps://github.com/go-playground/validator と組み合わせて複雑なバリデーションをしたい場合

x-oapi-codegen-extra-tags を書くことで任意のstruct tagをつけることができるので、タグベースのバリデーションはこれで行える バリデーションの種類はこれ go-playground/validator リクエストパラメータ向けValidationパターンまとめ - Qiita

paths:
  /hello:
    get:
      summary: Hello
      operationId: hello
      parameters:
        - name: sort-by
          in: query
          description: How to sort items
          required: false
          schema:
            type: string
          x-oapi-codegen-extra-tags:
            validate: len=5
        - name: datetime
          in: query
          schema:
            type: string
          x-oapi-codegen-extra-tags:
            validate: datetime_without_timezone

Echo + go-playground/validator

import (
	"my-application/openapi"
 
	"github.com/go-playground/validator/v10"
)
 
func main() {
	e := echo.New()
 
	swagger, err := openapi.GetSwagger()
	if err != nil {
		panic(err)
	}
	swagger.Servers = nil
	openapi.RegisterHandlers(e, handler{})
 
	// registor validator
	validate := validator.New()
	validate.RegisterValidation("datetime_without_timezone", validateDatetimeWithoutTimezone)
	e.Validator = &CustomValidator{validate: validate}
 
}
 
type CustomValidator struct {
	validate *validator.Validate
}
 
func (cv *CustomValidator) Validate(i interface{}) error {
	if err := cv.validate.Struct(i); err != nil {
		return echo.NewHTTPError(http.StatusBadRequest, err.Error())
	}
	return nil
}
 
func validateDatetimeWithoutTimezone(fl validator.FieldLevel) bool {
	date := fl.Field().String()
	_, err := time.Parse("2006-01-02T15:04:05", date)
	return err == nil
}
 
type handler struct {}
 
func (h handler) Hello(ec echo.Context, params openapi.HelloParams) error {
	if err := ec.Validate(params); err != nil {
		return err
	}
	return ec.String(http.StatusOK, fmt.Printf("Hello, %s", params.Name))
}
 

validatorのエラーメッセージをカスタムしたい

[golang]Echoでvalidatorのエラーを日本語に変換する方法 | CodeLab How can I define custom error message? · Issue #559 · go-playground/validator

tagに応じたメッセージを作成する、validator.ValidationErrors.Translate で翻訳する(用意された翻訳メッセージ)

package main
 
import (
	"errors"
	"log"
	"net/http"
	"reflect"
	"strings"
	"time"
 
	"github.com/go-playground/locales/ja_JP"
	ut "github.com/go-playground/universal-translator"
	"github.com/go-playground/validator/v10"
	ja_translations "github.com/go-playground/validator/v10/translations/ja"
	"github.com/labstack/echo/v4"
)
 
func main() {
	e := echo.New()
 
	validate := validator.New()
 
	// エラーメッセージに出力するフィールド名をタグから取得する
	validate.RegisterTagNameFunc(func(field reflect.StructField) string {
		fieldName := field.Tag.Get("field_ja")
		if fieldName == "-" {
			return ""
		}
		return fieldName
	})
 
	// 日本語のメッセージを出力する
	japanese := ja_JP.New()
	uni := ut.New(japanese, japanese)
	trans, _ := uni.GetTranslator("ja")
	err := ja_translations.RegisterDefaultTranslations(validate, trans)
	if err != nil {
		log.Fatal(err)
	}
	// メッセージを上書きする場合はこのようにする
	validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
		return ut.Add("required", "{0} を指定してください", true) // see universal-translator for details
	}, func(ut ut.Translator, fe validator.FieldError) string {
		t, _ := ut.T("required", fe.Field())
		return t
	})
 
	e.Validator = &CustomValidator{validate: validate, trans: trans}
 
}
 
type CustomValidator struct {
	trans    ut.Translator
	validate *validator.Validate
}
 
func (cv *CustomValidator) Validate(i interface{}) error {
	if err := cv.validate.Struct(i); err != nil {
		var ve validator.ValidationErrors
		msg := []string{}
		if errors.As(err, &ve) {
			for _, m := range ve.Translate(cv.trans) {
				msg = append(msg, m)
			}
		}
 
		return echo.NewHTTPError(http.StatusBadRequest, strings.Join(msg, ","))
	}
	return nil
}
 
type Params struct {
	Id *string `form:"id,omitempty" json:"id,omitempty" validate:"required" field_ja:"ユーザID"`
}
 

ユーザIDは必須フィールドです というエラーメッセージになる

試す

openapi: "3.1.0"
paths:
  /hello:
    get:
      summary: Sample API
      operationId: hello
      tags:
        - sample
      parameters:
        - name: name
          in: query
          required: true
          schema:
            type: string
            maxLength: 8
            minLength: 4
          description: your name
        - name: id
          in: query
          schema:
            type: string
            pattern: "^\\d{8}$"
          description: your id
        - name: age
          in: query
          schema:
            type: integer
            maximum: 30
            minimum: 10
          description: your name

こんなopenapi.yamlを作って適当なサーバーを立てて、範囲外の値を入力してやるとエラーが返された

localhost:8080/hello?name=alice&age=99

parameter "age" in query has an error: number must be at most 30

日付型のバリデーション

kin-openapiだけだとここで正規表現のみチェックしている https://github.com/getkin/kin-openapi/blob/e7d649f3f7d6ddbaaaed74a7d2f819a82118aab4/openapi3/schema.go#L943 https://github.com/getkin/kin-openapi/blob/e7d649f3f7d6ddbaaaed74a7d2f819a82118aab4/openapi3/schema_formats.go#L94

2022-01-02T15:04:05 みたいな文字列が通るはずと思ったが通らなかった。

oapi-codegen のmiddlewareではここで time.Parse できるかもチェックしているためだった。 https://github.com/deepmap/oapi-codegen/blob/ab90f1927bc5ec3e29af216d4298fbb4780ae36d/pkg/runtime/bindstring.go#L125

そのため独自フォーマットを用意してあげる必要がある。

 
import "github.com/getkin/kin-openapi/openapi3"
 
 
	openapi3.DefineStringFormat("custom-date", `^[0-9]{4}-(0[0-9]|10|11|12)-([0-2][0-9]|30|31)T[0-9]{2}:[0-9]{2}:[0-9]{2}$`)
  /hello:
    get:
      summary: Sample API
      operationId: hello
      tags:
        - admin
      parameters:
        - name: obj
          in: query
          schema:
            type: object
            properties:
              date:
                type: string
                format: date-time
          style: deepObject
          explode: true

再現できた。エラーメッセージのtimeがtimになってる http://localhost:1323/hello?obj[date]=2022-12-21T15:04:05

Invalid format for parameter obj: error assigning value to destination: error assigning field [date]: error parsing tim as RFC3339 or 2006-01-02 time: parsing time "2022-12-21T15:04:05": extra text: "T15:04:05"