Go の入力バリデーションパッケージ ozzo-validation を試した。
はじめに
Go のウェブアプリで使う入力バリデーションに関して、ozzo-validation
を検討した。
これまでのバリデーション
普段、仕事では labstack/echo
という Go のウェブフレームワークを使う事が多いのだけど、バリデーションに関しては labstack/echo
のサンプルに合わせて go-playground/validator
を使ってきた。
go-playground/validator
は機能も豊富で(一応)痒い所に手は届くのだけど、struct にタグを付けて判定させないといけない。これが実に煩わしい。以前 labstack/echo
を使ったサンプルを書いたので、それを見て欲しい。
// Comment is a struct to hold unit of request and response.
type Comment struct {
Id int64 `json:"id" db:"id,primarykey,autoincrement"`
Name string `json:"name" form:"name" db:"name,notnull,size:200"`
Text string `json:"text" form:"text" validate:"required,max=20" db:"text,notnull,size:399"`
Created time.Time `json:"created" db:"created,notnull"`
Updated time.Time `json:"updated" db:"updated,notnull"`
}
json
はまぁ分かる。これに ORM の為の db
と、validator の為の validate
が含まれており、記述が難しい。間違っても実行時までエラーにならない。また IDE の入力補完も効かない。辛い。
ORM のエラーには頼れない
実は最近、ORM 周りを ent
にスイッチした。
ent
はスキーマをコードで書く。なので struct tag を汚す事もない。こちらの ORM にも値のバリデーションはあるのだが、ent
が吐き出すエラーはあくまで DB のスキーマを定義する為の物。決してウェブの入力の為の物ではない。
ozzo-validation が良さそう
このまま仕方なく、struct tag を書いていくしかないのかな、と思っていたのだけど ozzo-validation
というのを見つけた。
実は新しい訳じゃなく、単に僕が見付けられていなかっただけで、結構人気もあるし、使われているみたい。ozzo-validation
は go-playground/validator
と異なり、struct tag を使わない。ルールはコードで書く。
package main
import (
"fmt"
"github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
)
func main() {
data := "example"
err := validation.Validate(data,
validation.Required, // not empty
validation.Length(5, 100), // length between 5 and 100
is.URL, // is a valid URL
)
fmt.Println(err)
// Output:
// must be a valid URL
}
実に潔く、間違いもエラー表示されるし、IDE の入力補完も効く。良い。
サンプルを作ってみた
試しに labstack/echo
と ozzo-validation
を使ってサンプルアプリケーションを作ってみた。
本体コードは短いのでそのまま貼る。
package main
import (
"embed"
"net/http"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
type Comment struct {
Name string `json:"name"`
Email string `json:"email"`
Content string `json:"content"`
}
type CustomValidator struct{}
func (cv *CustomValidator) Validate(i interface{}) error {
if c, ok := i.(validation.Validatable); ok {
return c.Validate()
}
return nil
}
func (a Comment) Validate() error {
return validation.ValidateStruct(&a,
validation.Field(
&a.Name,
validation.Required.Error("名前は必須入力です"),
validation.RuneLength(5, 20).Error("名前は 5~20 文字です"),
is.PrintableASCII.Error("名前はASCIIで入力して下さい"),
),
validation.Field(
&a.Email,
validation.Required.Error("メールアドレスは必須入力です"),
validation.RuneLength(5, 40).Error("メールアドレスは 5~40 文字です"),
is.Email.Error("メールアドレスを入力して下さい"),
),
validation.Field(
&a.Content,
validation.Required.Error("本文は必須入力です"),
validation.RuneLength(5, 50).Error("本文は 5~50 文字です"),
),
)
}
//go:embed static
var localFS embed.FS
func main() {
e := echo.New()
e.Debug = true
e.Validator = &CustomValidator{}
e.Use(middleware.Logger())
e.Use(middleware.StaticWithConfig(middleware.StaticConfig{
Root: "static",
Filesystem: http.FS(localFS),
}))
e.POST("/api", func(c echo.Context) error {
var comment Comment
if err := c.Bind(&comment); err != nil {
return err
}
if err := c.Validate(comment); err != nil {
errs := err.(validation.Errors)
for k, err := range errs {
c.Logger().Error(k + ": " + err.Error())
}
return err
}
return c.JSON(http.StatusOK, &struct {
Result string `json:"result"`
}{
Result: "OK",
})
})
e.Logger.Fatal(e.Start(":8989"))
}
labstack/echo
には元々、go-playground/validator
に依存しない、バリデーション機能を注入できる機能があり、Validate
メソッドを持った実装を与える事で自らバリデーションを実装できる。このコードの様に CustomValidator#Validate
を実装し、それが validation.Validatable
である場合には Validate
メソッドを呼び出す様にすれば良い。
現状の課題
課題が何もない訳ではない。日本人向けのウェブサービスだと、エラーメッセージも英語で出す必要がある。一応、ozzo-validation
にエラーメッセージを差し替える方法があるにはある。
func (a Comment) Validate() error {
return validation.ValidateStruct(&a,
validation.Field(
&a.Name,
validation.Required.Error("名前は必須入力です"),
validation.RuneLength(5, 20).Error("名前は 5~20 文字です"),
is.PrintableASCII.Error("名前はASCIIで入力して下さい"),
),
validation.Field(
&a.Email,
validation.Required.Error("メールアドレスは必須入力です"),
validation.RuneLength(5, 40).Error("メールアドレスは 5~40 文字です"),
is.Email.Error("メールアドレスを入力して下さい"),
),
validation.Field(
&a.Content,
validation.Required.Error("本文は必須入力です"),
validation.RuneLength(5, 50).Error("本文は 5~50 文字です"),
),
)
}
go-playground/validator
の場合は必須入力の翻訳メッセージをオフィシャルが用意してくれていた。ozzo-validation
の場合は自ら実装するしかない。かつ穴あき(上記で言う 5 や 50)をメッセージですり替えられる様にはなっていない。一応、上記の通り、項目毎にメッセージが差し替えられるし、コードでパラメータを確認できるので、独自の i18n 対応さえすれば実現できなくはない。
if err := c.Validate(comment); err != nil {
errs := err.(validation.Errors)
for k, err := range errs {
c.Logger().Error(k + ": " + err.Error())
verr := err.(validation.Error)
for k, v := range verr.Params() {
c.Logger().Errorf(" %s: %v", k, v)
}
}
return err
}
content: 本文は 5~50 文字です
min: 5
max: 50
おわりに
ozzo-validation
により
Pros
- struct tag が汚れるのを回避できた
- コードでバリデーションするので間違いに気付ける様になった
- コードでバリデーションするので IDE の補完が効く
Cons
- エラーメッセージの翻訳はデフォルトで翻訳されない
しかしコードで全て倒せる様になってるので、個人的には好き。
追記
以下の様に実装すれば、エラーを穴あきにできる事が分かりました。
あとはこれを i18n パッケージ(gettext が扱える様な)を使って、外部ファイルで翻訳すれば良さそうです。
Discussion