Go言語のバリデーションチェックライブラリ(ozzo-validation)を分かりやすくまとめてみた

# はじめに
APIサーバーの開発ではリクエストパラメータのバリデーションチェックは必須です。
Go言語ではバリデーションチェックのためのライブラリとして、 [go-playground/validator](https://github.com/go-playground/validator) と [go-ozzo/ozzo-validation](https://github.com/go-ozzo/ozzo-validation)が有名なようです。
この記事では、go-ozzo/ozzo-validationについてまとめます。
go-playground/validatorに関しては、[こちらの記事](https://qiita.com/RunEagler/items/ad79fc860c3689797ccc)が分かりやすかったです。

# ozzo-validationとは
Go言語のバリデーション用の外部パッケージです。
より知名度が高いうえにシンプルに使えるものとしては、`go-playground/validator`がありますが、こちらは構造体にバリデーションルールを記載する必要があり、OpenAPI(Swagger)における構造体の自動生成の恩恵を受けることができなくなってしまいます。（自動生成時に構造体が上書きされてしまうから。）
`ozzo-validation`はOpenAPIによる自動生成との親和性が良いためそこでの優位性があります。また、gRPCサーバの開発でも活用できます。

# インストール
```bash
$ go get github.com/go-ozzo/ozzo-validation
$ go get github.com/go-ozzo/ozzo-validation/is
```

# 具体例とその解説
具体例から入った方が理解しやすいと思いますので、いきなり具体例から入ります。

## 変数の例
stringの変数をバリデーションする最もシンプルな例です。
クエリパラメータやパスパラメータはこの形式でバリデーションするケースになるかと思います。

```go
package main

import (
	"fmt"

	validation "github.com/go-ozzo/ozzo-validation"
	"github.com/go-ozzo/ozzo-validation/is"
)

func main() {
	data := "example"
	err := validation.Validate(data,
		validation.Required,       // 空を許容しない
		validation.Length(5, 100), // 長さが5から100まで
		is.URL,                    // URL形式のみ
	)
	fmt.Println(err)
}
```

```bash:実行結果
$ go run main.go
must be a valid URL
```

`Validate` 関数でバリデーション実行する。第一引数に対象の変数を指定し、第二引数以降にルールを指定する。バリデーションを全て成功すればnilを返し、1つでも失敗すればエラーを返す。
ルールについては後述します。
上記の例では、変数dataがURL形式ではないので、バリデーションエラーとなっています。

## 構造体の例
jsonなどのボディ部に埋められたパラメータのバリデーションはこのケースになると思います。

```go
package main

import (
	"fmt"
	"regexp"

	validation "github.com/go-ozzo/ozzo-validation"
)

type Address struct {
	Street string
	City   string
	State  string
	Zip    string
}

func (a Address) Validate() error {
	return validation.ValidateStruct(&a,
		// Streetは空を許容せず、5から50までの長さ
		validation.Field(&a.Street, validation.Required, validation.Length(5, 50)),
		// Cityは空を許容せず、5から50までの長さ
		validation.Field(&a.City, validation.Required, validation.Length(5, 50)),
		// Stateは空を許容せず、大文字2つの文字列
		validation.Field(&a.State, validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
		// Stateは空を許容せず、数字5つの文字列
		validation.Field(&a.Zip, validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
	)
}

func main() {
	a := Address{
		Street: "123",
		City:   "Unknown",
		State:  "Virginia",
		Zip:    "12345",
	}

	err := a.Validate()
	fmt.Println(err)
}
```

```bash:実行結果
$ go run main.go
State: must be in a valid format; Street: the length must be between 5 and 50.
```

`ValidateStruct` 関数で構造体のバリデーションを行う。バリデーションを全て成功すればnilを返し、1つでも失敗すればエラーを返す。
`Field` 関数でフィールドのバリデーションを行う。呼び出す際は、第一引数に構造体のポインタを渡し、第二引数以降でルールを指定する。
ルールに関しては後述します。
もしバリデーションに失敗したフィールドがあれば、エラーは保存され、次のフィールドのバリデーションを行います。

# ルール
## よく使うもの
- In(...interface{}): リスト中に値があるかのチェック

```go
validation.Field(&c.Gender, validation.In("Female", "Male")),
```

- Length(min, max int): strings/slices/maps/arrays 型の長さが指定範囲内かのチェック

```go
validation.Field(&c.Name, validation.Required, validation.Length(5, 20)),
```

- Min(min interface{}) and Max(max interface{}): int/uint/float/time.Time型の値が指定範囲内かのチェック
- 引数で指定した値は有効範囲に含める。下記の例でいえば、1以上3以下が有効範囲となる。

```go
validation.Field(&a.Value, validation.Min(1), validation.Max(3)),
```

- Match(*regexp.Regexp): 正規表現にマッチしているかのチェック

```go:大文字2つで構成されているかどうか
validation.Field(&a.State, validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
```

- Date(layout string): 指定日付フォーマット形式かのチェック。日付は例のごとく `2006-01-02` でなければいけないので注意。

```go
validation.Field(&a.ShippedAt, validation.Date("2006-01-02")),
```

- Required: 空でないことのチェック。nil/""/0を許容しない。

```go
validation.Field(&a.Street, validation.Required)
```

- NotNil: ポインタの値がnilじゃないことのチェック。""/0は許容する。

```go
validation.Field(&a.SampleNotNil, validation.NotNil),
```

ここからisルール。
空の場合はエラーにならない。
注意点として、対象は全て文字列である必要がある。

- Email: Eメール形式かどうかをチェック。

```go
validation.Field(&c.Sample, is.Email),
```

- URL: URL形式かどうかをチェック。

```go
validation.Field(&c.Sample, is.URL),
```

- Alpha: 英字(a-zA-Z)のみで構成されているかチェック。

```go
validation.Field(&c.Sample, is.Alpha),
```

- Digit: 数字(0-9)のみで構成されているかチェック。

```go
validation.Field(&c.Sample, is.Digit),
```

- Alphanumeric: 英数字(a-zA-Z0-9)のみで構成されているかチェック。

```go
validation.Field(&c.Sample, is.Alphanumeric),
```

- LowerCase: 小文字のみで構成されているかチェック。

```go
validation.Field(&c.Sample, is.LowerCase),
```

- UpperCase: 大文字のみで構成されているかチェック。

```go
validation.Field(&c.Sample, is.UpperCase),
```

- Int: int型の文字のみで構成されているかチェック。

```go
validation.Field(&c.Sample, is.Int),
```

- Float: float型の文字のみで構成されているかチェック。

```go
validation.Field(&c.Sample, is.Float),
```

- UUID: UUID形式かどうかをチェック。

```go
validation.Field(&c.Sample, is.UUID),
```

- CreditCard: クレジットカード形式かどうかをチェック。

```go
validation.Field(&c.Sample, is.CreditCard),
```

- JSON: json形式かどうかをチェック。

```go
validation.Field(&c.Sample, is.JSON),
```

- MAC: MACアドレス形式かどうかをチェック。

```go
validation.Field(&c.Sample, is.MAC),
```

- IP: IPアドレス形式かどうかをチェック。

```go
validation.Field(&c.Sample, is.IP),
```

## カスタムルール
自分で関数を用意してルールを作成することもできます。
バリデーションチェックロジックの関数（下ではcheckAbc）を自前で用意し、`By`でそれを呼び出します。

```go
func checkAbc(value interface{}) error {
	s, _ := value.(string)
	if s != "abc" {
		return errors.New("must be abc")
	}
	return nil
}

err := validation.Validate("xyz", validation.By(checkAbc))
fmt.Println(err)
// Output: must be abc
```

# その他
## バリデーション中のinternal server error
バリデーション処理中のエラーがバリデーションルール違反なのかinternal server errorなのかを切り分けられるようにするためには、以下のように実装する。

```go
if err := a.Validate(); err != nil {
	if e, ok := err.(validation.InternalError); ok {
		log.Println(e.InternalError())
	}
}
```

# 参考
https://github.com/go-ozzo/ozzo-validation