概要
HTTPやgRPCなどのAPIでvalidationを実装する際、次のような課題に直面することがよくあります。
- validationの実装を忘れる
- 正常系だけを考え、異常系のイメージがない
- validationがどこに実装されているか分からない
- クライアント側との認識がずれたり、テスト漏れが起きやすい
- validationをどこに実装するか悩む
- オニオンアーキテクチャでいう「インフラ層・ユースケース層・ドメイン層」のどこに置くべきか、チーム内で議論になりがち
もしProtobufを使ってAPI定義を行っている(スキーマ駆動開発)なら、validationの責務もProtobuf側に寄せてしまうという設計が有効です。
この記事では、そのためのツールである proto-gen-validate を使い、Protobufスキーマでvalidationを定義・自動生成する方法を紹介します。
環境
- proto-gen-validate v1.2.1
- Go 1.25.1
proto-gen-validate
proto-gen-validate とは
proto-gen-validate は、Protobufで定義したメッセージに対して、バリデーションルールを宣言的に記述できるツールです。
スキーマに直接制約条件を記述することで、サーバー・クライアント双方で一貫した入力検証を自動化できます。
基本的な使い方
まずは簡単な使い方を紹介します。
記述フォーマット
記述フォーマットは以下です。
// 制約条件が1つの場合 [(validate.rules).<型>.<条件> = <値>]; // 制約条件が複数の場合、オブジェクト的に記述 [(validate.rules).<型> = {<条件1>: <値>, <条件2>: <値>}];
具体的にはこのように書きます。
// 20文字以下 string x = 1 [(validate.rules).string.max_len = 20]; // 5文字以上、10文字以下 string x = 1 [(validate.rules).string = {min_len: 5, max_len: 10}];
また、「オプションだから無くても良いが、使う場合は制約したい」といった場合はignore_emptyを併用します。
string x = 1 [(validate.rules).string = {ignore_empty: true, len: 2}];
使い方
スキーマ定義
まず以下の様にスキーマ定義をします。
syntax = "proto3"; package examplepb; import "validate/validate.proto"; message User { string name = 1 [(validate.rules).string = {min_len: 1, max_len: 20}]; string email = 2 [(validate.rules).string.email = true]; uint32 age = 3 [(validate.rules).uint32 = {gte: 0, lte: 120}]; }
コンパイル方法
コンパイルは次のようにパラメータを指定します。
protoc \ -I . \ -I $(GOPATH)/pkg/mod/github.com/envoyproxy/protoc-gen-validate@v1.2.1 \ --go_out=":../generated" \ --validate_out="lang=go:../generated" \ example.proto
Go言語でprotocを使う場合に何かしらハマった場合は、以下の記事を参考にすると解決できると思います。
protocのパラメータ指定でハマった場合はこちら↓
go_package周りでハマった場合はこちら↓
実装
実際にvalidationする際は次のように利用します。
package main import ( "fmt" examplepb "path/to/gen" ) func main() { u := &examplepb.User{} err := u.Validate() fmt.Println(err) // err: 名前がない u.Name = "Alice" err = u.Validate() fmt.Println(err) // err: メールアドレスが不適切 u.Email = "alice@example.com" u.Age = 999 err = u.Validate() fmt.Println(err) // err: 年齢が不適切 u.Age = 30 err = u.Validate() fmt.Println(err) // nil: OK }
よく使う制約条件
次によく使う制約条件の書き方を紹介します。
Numerics
数字に対する制約です。
lt/lte/gt/gte
// 小なり(<) int32 x = 1 [(validate.rules).int32.lt = 10]; // 大なりイコール(≧) uint64 x = 1 [(validate.rules).uint64.gte = 20]; // 範囲指定 fixed32 x = 1 [(validate.rules).fixed32 = {gte:30, lt: 40}];
in/not_in
// 指定した数字のどれか uint32 x = 1 [(validate.rules).uint32 = {in: [1,2,3]}]; // 指定した数字がNG float x = 1 [(validate.rules).float = {not_in: [0, 0.99]}];
String
Stringに対する制約です。
文字数指定
// 文字数固定 string x = 1 [(validate.rules).string.len = 5]; // 何文字以上 string x = 1 [(validate.rules).string.min_len = 3]; // 文字数の範囲指定 string x = 1 [(validate.rules).string = {min_len: 5, max_len: 10}];
パターンマッチング
// x must be a non-empty, case-insensitive hexadecimal string string x = 1 [(validate.rules).string.pattern = "(?i)^[0-9a-f]+$"];
prefix, suffix, contain,
// prefix string x = 1 [(validate.rules).string.prefix = "foo"]; // suffix string x = 1 [(validate.rules).string.suffix = "bar"]; // x must contain "baz" anywhere inside it string x = 1 [(validate.rules).string.contains = "baz"]; // x cannot contain "baz" anywhere inside it string x = 1 [(validate.rules).string.not_contains = "baz"]; // x must begin with "fizz" and end with "buzz" string x = 1 [(validate.rules).string = {prefix: "fizz", suffix: "buzz"}]; // x must end with ".proto" and be less than 64 characters string x = 1 [(validate.rules).string = {suffix: ".proto", max_len:64}];
よく使われるフォーマット
// メールアドレス (RFC 5322準拠) string x = 1 [(validate.rules).string.email = true]; // ホスト名(RFC 1034準拠) string x = 1 [(validate.rules).string.hostname = true]; // IPアドレス (v4 or v6) string x = 1 [(validate.rules).string.ip = true]; // IPv4アドレス // 例: "192.168.0.1" string x = 1 [(validate.rules).string.ipv4 = true]; // IPv6アドレス // 例: "fe80::3" string x = 1 [(validate.rules).string.ipv6 = true]; // URI (RFC 3986準拠) string x = 1 [(validate.rules).string.uri = true]; // UUID (RFC 4122準拠) string x = 1 [(validate.rules).string.uuid = true];
Messages
独自の型(オブジェクト・構造体)に対する制約です。
必須条件
Person x = 1 [(validate.rules).message.required = true];
Repeated
配列・スライスに対する制約です。
要素数
// 最低数 repeated int32 x = 1 [(validate.rules).repeated.min_items = 3]; // 範囲指定 repeated Person x = 1 [(validate.rules).repeated = {min_items: 5, max_items: 10}]; // 固定数 repeated double x = 1 [(validate.rules).repeated = {min_items: 7, max_items: 7}];
ユニーク
repeated int64 x = 1 [(validate.rules).repeated.unique = true];
要素に対する制約
// 各要素0以上 repeated float x = 1 [(validate.rules).repeated.items.float.gt = 0];
その他
サンプルコード
今回のサンプルコードはこちら
まとめ
proto-gen-validateの使い方を説明しました。
これを使うことでAPIの入力チェックをスキーマに統合し、サーバー・クライアント間で一貫したバリデーションを自動化できるようになります。