入力検証の基本
このレッスンでは、Bean Validation が何をしているのかを理解し、必須・長さ・範囲・形式という基本的な検証を自分で実装できるようになります。
検証をコントローラに書くとどうなるでしょうか
検証を素直に手で書くと、次のようになります。
Java
@PostMapping("/users")
public String create(@RequestBody UserRequest req) {
if (req.getName() == null || req.getName().isBlank()) {
return "name is required";
}
if (req.getName().length() > 20) {
return "name is too long";
}
if (req.getAge() < 0 || req.getAge() > 150) {
return "age is out of range";
}
// 本題はここから
}やりたいことは「利用者を1人作る」ことなのに、その前に if が並びます。同じ検証を別のエンドポイントでも書けば、当然そこでも重複します。
Bean Validation は制約を型の側に書きます
Jakarta Bean Validation は、検証のルールをクラスのフィールドにアノテーションとして書く仕組みです。Spring Boot 3.x では jakarta.validation パッケージにあり、spring-boot-starter-validation を依存に追加すると使えるようになります。
Java
public class UserRequest {
@NotBlank
@Size(max = 20)
private String name;
@Min(0)
@Max(150)
private int age;
@NotBlank
@Email
private String email;
}こう書いておくと、ルールがフィールドの隣に置かれます。「name は必須で20文字以内」という仕様が、コードを読むだけで分かります。
@Valid が実行の合図です
制約を書いただけでは検証は走りません。コントローラの引数に @Valid を付けて、はじめて Spring が検証を実行します。
Java
@PostMapping("/users")
public UserResponse create(@Valid @RequestBody UserRequest req) {
// ここに来た時点で、制約はすべて満たされている
}Spring は本文を UserRequest に変換したあと、@Valid が付いているのを見て Validator を呼び出します。違反があればメソッドを呼ばず、既定では MethodArgumentNotValidException を投げ、400 Bad Request として応答します。つまりメソッドの中に入れた時点で、制約はすべて通っていると考えてよいわけです。この「前提を型で保証する」性質が、if の羅列を消してくれます。
| 目的 | 主なアノテーション |
|---|---|
| 必須 | @NotNull、@NotEmpty、@NotBlank |
| 長さ | @Size(min, max) |
| 数の範囲 | @Min、@Max |
| 形式 | @Email、@Pattern |
演習で書くもの
演習では、その Validator の中身にあたる判断を自分で書きます。名前・年齢・メールを受け取り、必須、長さ、範囲、形式の順に確かめ、最初に見つかったエラーの名前を返します。すべて通れば ok を返します。Spring では @Valid を1つ付けるだけで自動的に行われる処理を、ここでは順序と判定条件を含めて自分の手で組み立てます。次のレッスンでは、これを「最初の1件」ではなく「全部まとめて」返す形に発展させます。
要件
- 検証の順序は name の必須、name の長さ、age の範囲、email の必須、email の形式とし、最初に違反したものだけを返すこと
- エラー名は name_required、name_too_long、age_out_of_range、email_required、email_invalid のいずれかとすること
- 違反がなければ ok を返すこと
入出力例
validate("taro", 20, "taro@example.com") → "ok"
validate(" ", 20, "taro@example.com") → "name_required"
validate("abcdefghijklmnopqrstu", 20, "taro@example.com") → "name_too_long"
validate("taro", 200, "taro@example.com") → "age_out_of_range"
validate("taro", -1, "taro@example.com") → "age_out_of_range"
validate("taro", 20, "") → "email_required"
validate("taro", 20, "taro.example.com") → "email_invalid"
validate("taro", 20, "@example.com") → "email_invalid"
validate("taro", 0, "a@b") → "ok"