Spring Validator入門:サンプルコードで学ぶバリデーションの基礎
1. はじめに
Webアプリケーションを開発する上で、「バリデーション(Validation)」は避けては通れない、非常に重要な要素です。ユーザーがフォームから送信するデータが、私たちが期待する形式やルールに沿っているかを確認するこのプロセスは、アプリケーションの品質と信頼性を左右します。
なぜバリデーションは重要なのでしょうか?
- データ整合性の確保: データベースに不正なデータや意図しない形式のデータが保存されるのを防ぎます。例えば、年齢フィールドに文字列が入ったり、必須項目が空のまま登録されたりする事態を未然に防ぎます。
- セキュリティの向上: 不正な入力値による攻撃(クロスサイトスクリプティングやSQLインジェクションなど)のリスクを低減します。バリデーションはセキュリティ対策の第一歩です。
- ユーザーエクスペリエンス (UX) の向上: ユーザーが間違ったデータを入力した場合、即座に「どこが」「なぜ」間違っているのかを分かりやすくフィードバックすることで、ユーザーはストレスなくフォームを修正できます。これにより、アプリケーションの使いやすさが格段に向上します。
JavaのWebアプリケーションフレームワークであるSpring Frameworkは、この重要なバリデーション処理を効率的かつ柔軟に実装するための強力な機能を提供しています。特に、Spring Bootと組み合わせることで、最小限の設定で高度なバリデーションを導入することが可能です。
この記事では、Spring Frameworkが提供するバリデーション機能、特に「Bean Validation (JSR-380)」とSpring独自の「Validator
インターフェース」に焦点を当て、その基礎から応用までを豊富なサンプルコードと共に徹底的に解説します。
この記事で学べること:
- Springにおけるバリデーションの基本的な考え方
- アノテーションベースの簡単なバリデーション (Bean Validation) の実装方法
- ユーザーフレンドリーなエラーメッセージのカスタマイズ
- より複雑な要件に対応する応用的なバリデーション(グループ化、カスタムバリデーション)
- プログラムで柔軟なバリデーションを実装する
Validator
インターフェースの使い方 - REST APIにおけるバリデーションとエラーハンドリング
この記事を最後まで読み終える頃には、あなたはSpring Validatorを使いこなし、堅牢で使いやすいWebアプリケーションを構築するための確かなスキルを身につけていることでしょう。
2. Spring Validatorとは? 2つのアプローチ
Spring Frameworkでは、バリデーションを実装するための主要なアプローチが2つ提供されています。それぞれの特徴を理解し、プロジェクトの要件に応じて適切に使い分けることが重要です。
アプローチ1: Bean Validation (JSR-380 / Jakarta Bean Validation)
これは、Javaの標準仕様として定められているバリデーションの仕組みです。現在広く使われているのはJSR-380 (Bean Validation 2.0) であり、Jakarta EEへの移行に伴い「Jakarta Bean Validation」と呼ばれています。
特徴:
- アノテーションベース:
@NotNull
や@Size
といったアノテーションを、検証したいクラスのフィールドに付与するだけでバリデーションルールを宣言できます。 - 宣言的で直感的: コードが非常に簡潔になり、どのようなバリデーションが行われるかが一目でわかります。
- 標準仕様: Springに限定されず、JPA (Hibernate) など、他の多くのフレームワークでもサポートされています。
- 拡張性: 標準で提供されるアノテーションで不足する場合は、独自のアノテーションを作成することも可能です。
Spring Bootでは spring-boot-starter-validation
を依存関係に追加するだけで、このBean Validationの仕組みをすぐに利用できます。ほとんどの基本的なバリデーション要件は、このアプローチでカバーできます。初心者はまず、このBean Validationから学ぶことを強くお勧めします。
アプローチ2: Spring Validator
インターフェース
これは、Spring Frameworkが独自に提供しているバリデーションの仕組みです。
特徴:
- プログラム的: Javaコードでバリデーションロジックを直接記述します。
- 高い柔軟性: 複数のフィールドをまたいだ複雑な相関チェック(例: 「パスワードと確認用パスワードが一致するか」)や、データベースへの問い合わせを伴うバリデーション(例: 「このユーザー名は既に使われていないか」)など、ビジネスロジックに深く関わる検証を実装するのに適しています。
- DIコンテナとの親和性: ValidatorクラスをSpringのコンポーネントとして登録すれば、ServiceやRepositoryといった他のBeanをインジェクションして利用できます。
どちらを使うべきか?
一般的なベストプラクティスは、これら2つのアプローチを組み合わせる「ハイブリッドアプローチ」です。
- 単一フィールドの基本的なチェック(必須チェック、文字数、フォーマットなど)は、Bean Validationのアノテーションで行います。これにより、コードの大部分をシンプルで読みやすく保てます。
- Bean Validationだけでは実現できない複雑な相関チェックやビジネスロジックが必要な場合に限り、
Validator
インターフェースやカスタムバリデーションアノテーションで補います。
この記事では、まず最も一般的なBean Validationから詳しく解説し、その後でValidator
インターフェースを使ったプログラム的な実装方法についても掘り下げていきます。
3. 環境構築
それでは、実際に手を動かしながら学ぶために、Spring Bootプロジェクトを準備しましょう。
Spring Bootプロジェクトの作成
start.spring.io
を利用するのが最も簡単です。
- https://start.spring.io/ にアクセスします。
- 以下の項目を設定します。
- Project: Maven Project (または Gradle)
- Language: Java
- Spring Boot: 最新の安定版 (例: 3.x.x)
- Project Metadata:
- Group:
com.example
- Artifact:
spring-validator-demo
- Name:
spring-validator-demo
- Packaging: Jar
- Java: 17 or later
- Group:
- Dependencies:
Spring Web
Thymeleaf
(画面表示用)Validation
GENERATE
ボタンをクリックし、zipファイルをダウンロードします。- ダウンロードしたzipファイルを解凍し、お好みのIDE(IntelliJ IDEA, Eclipse, VS Codeなど)でプロジェクトを開きます。
依存関係の確認
Mavenを使用する場合、pom.xml
に以下の依存関係が含まれていることを確認してください。
pom.xml
“`xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
``
spring-boot-starter-validation` がBean Validation (Hibernate Validator) を利用するために必要なライブラリをまとめて導入してくれます。
これで、バリデーションを学ぶための準備が整いました。
4. Bean Validation (JSR-380) による基本的なバリデーション
ここからは、最も基本的かつ強力なBean Validationの使い方を、ユーザー登録フォームを例に学んでいきましょう。
主なバリデーションアノテーション
Bean Validationでは、以下のような様々なアノテーションが標準で用意されています。
アノテーション | 説明 | 対象 |
---|---|---|
文字列・コレクション関連 | ||
@NotNull |
null を許容しない。空文字列 ("" ) は許容する。 |
すべてのオブジェクト |
@NotEmpty |
null および空(サイズが0)を許容しない。 |
CharSequence , Collection , Map , Array |
@NotBlank |
null 、空文字列、および空白のみの文字列を許容しない。 |
CharSequence (文字列) |
@Size(min=, max=) |
文字列長やコレクションの要素数が指定範囲内か検証する。 | CharSequence , Collection , Map , Array |
@Pattern(regexp=) |
指定した正規表現に一致するか検証する。 | CharSequence |
@Email |
メールアドレスの形式として妥当か検証する。 | CharSequence |
数値関連 | ||
@Min(value) |
指定した値以上であるか検証する。 | Number |
@Max(value) |
指定した値以下であるか検証する。 | Number |
@Positive |
正の数であるか検証する。 | Number |
@PositiveOrZero |
0または正の数であるか検証する。 | Number |
@Negative |
負の数であるか検証する。 | Number |
@NegativeOrZero |
0または負の数であるか検証する。 | Number |
日付・時刻関連 | ||
@Past |
過去の日付・時刻であるか検証する。 | Date , Calendar , Java 8 Date/Time API |
@PastOrPresent |
過去または現在の日付・時刻であるか検証する。 | 同上 |
@Future |
未来の日付・時刻であるか検証する。 | 同上 |
@FutureOrPresent |
未来または現在の日付・時刻であるか検証する。 | 同上 |
その他 | ||
@AssertTrue / @AssertFalse |
boolean 型のフィールドがtrue /false であるか検証する。 |
boolean , Boolean |
これらのアノテーションを組み合わせることで、多くの入力チェックを宣言的に実装できます。
サンプルコードによる実装
Step 1: Formクラス(DTO)の作成
まず、クライアント(ブラウザ)とサーバー(コントローラー)間でデータをやり取りするためのクラスを作成します。これはFormクラスやDTO (Data Transfer Object) と呼ばれます。このクラスのフィールドにバリデーションアノテーションを付与します。
src/main/java/com/example/springvalidatordemo/form/UserForm.java
“`java
package com.example.springvalidatordemo.form;
import jakarta.validation.constraints.*;
public class UserForm {
@NotBlank // null, 空文字, 空白のみの文字列を許可しない
@Size(min = 2, max = 20) // 文字列長が2文字以上20文字以下
private String name;
@NotNull // nullを許可しない
@Min(0) // 0以上
@Max(150) // 150以下
private Integer age;
@NotBlank
@Email // Eメール形式であること
private String email;
@Pattern(regexp = "^(https?://)?([\\da-z.-]+)\\.([a-z.]{2,6})([/\\w .-]*)*/?$", message = "有効なURL形式で入力してください。")
private String website;
// Getter and Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getWebsite() {
return website;
}
public void setWebsite(String website) {
this.website = website;
}
}
``
@Pattern
ここでは、各フィールドに適切なアノテーションを付与して、バリデーションルールを定義しています。のように、
message`属性で個別にエラーメッセージを指定することも可能です。
Step 2: Controllerクラスの作成
次に、このUserForm
を受け取り、バリデーションを実行するControllerを作成します。
src/main/java/com/example/springvalidatordemo/controller/UserController.java
“`java
package com.example.springvalidatordemo.controller;
import com.example.springvalidatordemo.form.UserForm;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class UserController {
// フォーム画面を初期表示
@GetMapping("/user/register")
public String showForm(@ModelAttribute UserForm userForm) {
// @ModelAttributeにより、空のUserFormオブジェクトが自動的にModelに追加される
return "user/registerForm";
}
// フォーム送信を受け取り、バリデーションを実行
@PostMapping("/user/register")
public String processForm(@Valid @ModelAttribute UserForm userForm, BindingResult bindingResult, Model model) {
// バリデーション結果のチェック
if (bindingResult.hasErrors()) {
// エラーがあった場合、フォーム画面に戻る
return "user/registerForm";
}
// バリデーション成功時の処理
// (ここでは成功画面にリダイレクトする)
return "redirect:/user/success";
}
// 成功画面
@GetMapping("/user/success")
public String showSuccess() {
return "user/success";
}
}
“`
重要なポイント:
@Valid
アノテーション: バリデーションを実行したいメソッド引数(ここではUserForm
)の前に@Valid
を付与します。これにより、Spring MVCはUserForm
に設定されたアノテーションに基づいてバリデーションを自動的に実行します。BindingResult
引数:@Valid
を付けた引数の 直後 にBindingResult
型の引数を置きます。このオブジェクトにバリデーションの結果(エラー情報)が格納されます。BindingResult
を引数に取ることで、バリデーションエラーが発生しても例外がスローされず、コントローラー内でエラー処理を続行できます。bindingResult.hasErrors()
: このメソッドでバリデーションエラーの有無を判定します。エラーがあればtrue
を返し、フォーム画面(user/registerForm
)を再表示します。この時、userForm
オブジェクトにはユーザーが入力した値とエラー情報が保持されたままなので、入力内容を維持した状態でエラーメッセージを表示できます。@ModelAttribute
:showForm
メソッドでは、引数に@ModelAttribute
を付けることで、空のUserForm
インスタンスを生成し、Model
に “userForm” という名前で追加します。これにより、Thymeleafテンプレート側でth:object="${userForm}"
が利用可能になります。processForm
メソッドでも同様に、POSTされたデータをUserForm
オブジェクトにバインドし、Modelに追加します。
Step 3: View(Thymeleaf)の作成
最後に、フォームとエラーメッセージを表示するためのHTMLテンプレートをThymeleafを使って作成します。
src/main/resources/templates/user/registerForm.html
“`html
ユーザー登録フォーム
“`
src/main/resources/templates/user/success.html
“`html
ユーザー登録が完了しました!
“`
Thymeleafの重要なポイント:
th:object="${userForm}"
:<form>
タグで、コントローラーから渡されたuserForm
オブジェクトをバインドします。th:field="*{fieldName}"
: 各<input>
タグで、th:object
で指定したオブジェクトのプロパティ(フィールド)に紐付けます。*{...}
はth:object
への相対パスを示します。th:field
はid
,name
,value
属性を自動的に設定してくれる便利な属性です。#fields.hasErrors('fieldName')
:th:if
と組み合わせて、指定したフィールドにエラーがあるかどうかを判定します。th:errors="*{fieldName}"
: 指定したフィールドのエラーメッセージを表示します。#fields.hasErrors
でチェックした上で表示するのが一般的です。
これでアプリケーションを実行し、http://localhost:8080/user/register
にアクセスしてみてください。何も入力せずに登録ボタンを押したり、不正な値を入力したりすると、各フィールドの下にエラーメッセージが表示されるはずです。
5. エラーメッセージのカスタマイズ
デフォルトのエラーメッセージは英語であり、開発者には分かりますが、エンドユーザーには不親切です。「must not be blank」ではなく、「名前は必須です」と表示したいでしょう。エラーメッセージのカスタマイズにはいくつかの方法があります。
方法1: アノテーションの message
属性(非推奨)
UserForm
の @Pattern
で行ったように、アノテーションに直接メッセージを記述できます。
java
@NotBlank(message = "名前は入力必須です。")
private String name;
手軽ですが、メッセージがJavaコード内にハードコーディングされてしまうため、メッセージの変更に再コンパイルが必要になったり、国際化(多言語対応)が困難になったりするデメリットがあります。
方法2: メッセージプロパティファイルによる一元管理(推奨)
より柔軟で推奨される方法は、メッセージを外部のプロパティファイルで管理する方法です。Spring Bootでは、src/main/resources/messages.properties
というファイルを作成するだけで、自動的に読み込まれます。
src/main/resources/messages.properties
“`properties
— デフォルトメッセージのオーバーライド —
アノテーション名=メッセージ
NotBlank=必須項目です。
Email=有効なメールアドレス形式で入力してください。
Size=サイズは {min} から {max} の間でなければなりません。
Min={value}以上の値を入力してください。
Max={value}以下の値を入力してください。
— より詳細なキーによるオーバーライド —
アノテーション名.オブジェクト名.フィールド名
NotBlank.userForm.name=お名前を入力してください。
Size.userForm.name={min}文字以上、{max}文字以下で入力してください。
アノテーション名.フィールド名
Min.age=年齢は0歳以上で入力してください。
“`
Springは、以下の順序で適用するメッセージキーを探します。優先度が高い順に並んでいます。
Annotation.objectName.fieldName
(例:NotBlank.userForm.name
)Annotation.fieldName
(例:Min.age
)Annotation.fieldType
(例:NotNull.java.lang.Integer
)Annotation
(例:NotBlank
)
この仕組みにより、特定のフォームの特定のフィールドだけに適用する詳細なメッセージと、アプリケーション全体で共通の汎用的なメッセージをうまく使い分けることができます。
プレースホルダー:
{min}
, {max}
, {value}
, {regexp}
のように、アノテーションの属性値をメッセージ内で利用できるプレースホルダーが用意されています。これにより、「{min}文字以上」といった動的なメッセージを生成できます。
この messages.properties
ファイルを作成した後、UserForm
クラスの message
属性を削除して再度アプリケーションを実行してみてください。プロパティファイルで定義した日本語のメッセージが表示されるはずです。
6. 発展的なバリデーション
基本的なバリデーションができるようになったら、次はより複雑な要件に対応するためのテクニックを学びましょう。
グループ化 (Validation Groups)
同じFormオブジェクトを、シナリオによって異なるバリデーションルールで検証したい場合があります。例えば、ユーザーの「新規登録」と「プロフィール更新」で同じUserForm
を使い回すケースを考えます。
- 新規登録時: 名前、メールアドレス、パスワードはすべて必須。
- 更新時: 名前、メールアドレスは必須だが、パスワードは変更する場合のみ入力(空でもOK)。
このような場合に役立つのが「グループ化」機能です。
Step 1: マーカーインターフェースの作成
バリデーショングループを定義するための、中身が空のインターフェースを作成します。
src/main/java/com/example/springvalidatordemo/validation/group/OnCreate.java
“`java
package com.example.springvalidatordemo.validation.group;
import jakarta.validation.groups.Default;
// Defaultグループを継承することで、グループ指定なしのバリデーションも同時に実行される
public interface OnCreate extends Default {
}
“`
src/main/java/com/example/springvalidatordemo/validation/group/OnUpdate.java
“`java
package com.example.springvalidatordemo.validation.group;
import jakarta.validation.groups.Default;
public interface OnUpdate extends Default {
}
``
jakarta.validation.groups.Defaultは、グループが指定されなかった場合に実行されるデフォルトのグループです。これを継承することで、グループ指定をした際に、グループ指定のない(
groups`属性を持たない)アノテーションも一緒に検証対象に含めることができます。
Step 2: Formクラスへの適用
groups
属性を使って、どのアノテーションがどのグループに属するかを指定します。
UserForm.java
(一部抜粋・修正)
“`java
import com.example.springvalidatordemo.validation.group.OnCreate;
// … other imports
public class UserForm {
// ... name, age, email フィールド (groups属性なし、つまりDefaultグループ)
@NotBlank(groups = OnCreate.class) // 新規登録時のみ必須
private String password;
// Getter, Setter ...
}
“`
Step 3: Controllerでのグループ指定
Controller側では、@Valid
の代わりにSpring独自の @Validated
アノテーションを使い、実行したいバリデーショングループを指定します。
UserController.java
(一部抜粋・修正)
“`java
// … imports
import com.example.springvalidatordemo.validation.group.OnCreate;
import org.springframework.validation.annotation.Validated;
@Controller
public class UserController {
// ... showForm, showSuccess メソッド
// 新規登録処理 (OnCreateグループを指定)
@PostMapping("/user/register")
public String processCreateForm(@Validated(OnCreate.class) @ModelAttribute UserForm userForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "user/registerForm";
}
return "redirect:/user/success";
}
// 更新処理 (ここではOnUpdateグループを仮定。実装は省略)
// @PostMapping("/user/update")
// public String processUpdateForm(@Validated(OnUpdate.class) @ModelAttribute UserForm userForm, BindingResult bindingResult) { ... }
}
``
@Validated(OnCreate.class)と指定することで、
groups = OnCreate.classが付与されたアノテーションと、
groups属性が指定されていないアノテーション(
Default`グループ)の両方が検証されます。これにより、新規登録時にはパスワードが必須となり、更新時にはパスワードのチェックはスキップされる、という制御が実現できます。
@Valid
はJSR標準のアノテーションでグループ化機能はありません。@Validated
はSpring独自のアノテーションで、グループ化機能を提供します。これが両者の大きな違いです。
カスタムバリデーション (Custom Validator)
標準のアノテーションでは表現できない、より複雑なバリデーションルールを実装したい場合は、独自のバリデーションアノテーションを作成できます。
よくある例が「パスワードと確認用パスワードの一致チェック」です。これは2つのフィールドを比較する必要があるため、単一フィールドに付けるアノテーションでは実装できません。クラスレベルで適用するカスタムアノテーションを作成します。
Step 1: カスタムバリデーションアノテーションの作成
まず、@PasswordEquals
のような独自のアノテーションを定義します。
src/main/java/com/example/springvalidatordemo/validation/PasswordEquals.java
“`java
package com.example.springvalidatordemo.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE}) // クラスレベルで適用
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PasswordEqualsValidator.class) // バリデーションロジックのクラスを指定
public @interface PasswordEquals {
String message() default “パスワードと確認用パスワードが一致しません。”;
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String passwordFieldName() default "password";
String confirmPasswordFieldName() default "confirmPassword";
}
“`
ポイント:
@Constraint(validatedBy = ...)
: このアノテーションを実装するバリデータクラスを指定します。これが核心部分です。@Target({ElementType.TYPE, ...})
: このアノテーションをどこに付けられるかを指定します。ElementType.TYPE
はクラス、インターフェース、enumに付けられることを意味します。@Retention(RetentionPolicy.RUNTIME)
: アノテーション情報を実行時まで保持するようにします。message
,groups
,payload
の3つの属性は、規約として定義する必要があります。passwordFieldName
,confirmPasswordFieldName
のような独自の属性を追加して、バリデータクラスに値を渡すこともできます。
Step 2: バリデーションロジックの実装
次に、ConstraintValidator
インターフェースを実装して、実際のバリデーションロジックを記述します。
src/main/java/com/example/springvalidatordemo/validation/PasswordEqualsValidator.java
“`java
package com.example.springvalidatordemo.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.BeanWrapperImpl;
public class PasswordEqualsValidator implements ConstraintValidator
private String passwordFieldName;
private String confirmPasswordFieldName;
private String message;
@Override
public void initialize(PasswordEquals constraintAnnotation) {
this.passwordFieldName = constraintAnnotation.passwordFieldName();
this.confirmPasswordFieldName = constraintAnnotation.confirmPasswordFieldName();
this.message = constraintAnnotation.message();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
// valueはアノテーションが付与されたオブジェクト(今回はUserForm)
BeanWrapperImpl beanWrapper = new BeanWrapperImpl(value);
String password = (String) beanWrapper.getPropertyValue(passwordFieldName);
String confirmPassword = (String) beanWrapper.getPropertyValue(confirmPasswordFieldName);
if (password == null || confirmPassword == null) {
// 片方または両方がnullの場合は、他の@NotNullなどでチェックされるべきなのでここではtrueを返す
return true;
}
boolean matched = password.equals(confirmPassword);
if (!matched) {
// エラーメッセージをデフォルトではなく、確認用パスワードのフィールドに関連付ける
context.disableDefaultConstraintViolation(); // デフォルトの制約違反情報を無効化
context.buildConstraintViolationWithTemplate(message) // 新しい制約違反情報を作成
.addPropertyNode(confirmPasswordFieldName) // エラーを関連付けるプロパティを指定
.addConstraintViolation();
}
return matched;
}
}
“`
ポイント:
ConstraintValidator<PasswordEquals, Object>
: 最初の型引数は対応するアノテーション、2つ目は検証対象の型です。クラスレベルのアノテーションなので、対象は特定のクラス(UserForm
など)を含むObject
になります。initialize()
: バリデーションが実行される前に一度だけ呼ばれ、アノテーションの属性値を取得できます。isValid()
: ここにバリデーションロジックを記述します。true
を返せば成功、false
を返せば失敗です。BeanWrapperImpl
: Springが提供するユーティリティで、JavaBeansのプロパティに名前でアクセスするのに便利です。- エラーメッセージの関連付け: デフォルトではクラスレベルのエラーになりますが、
context.buildConstraintViolationWithTemplate
を使うことで、特定フィールド(今回は確認用パスワード)にエラーメッセージを関連付けることができ、UXが向上します。
Step 3: Formクラスへの適用
最後に、作成したアノテーションをFormクラスに付与します。
UserForm.java
(一部抜粋・修正)
“`java
// … imports
import com.example.springvalidatordemo.validation.PasswordEquals;
@PasswordEquals(groups = OnCreate.class) // クラスレベルにアノテーションを付与
public class UserForm {
// … name, age, email
@NotBlank(groups = OnCreate.class)
private String password;
@NotBlank(groups = OnCreate.class)
private String confirmPassword;
// Getters and Setters...
}
``
@Validated(OnCreate.class)` でバリデーションを実行した際に、パスワードと確認用パスワードの一致チェックも行われるようになります。Thymeleaf側では、確認用パスワードのフィールドでエラーメッセージを表示するようにしておけばOKです。
これで、
7. Spring Validator
インターフェースによるプログラム的バリデーション
アノテーションベースのBean Validationは非常に便利ですが、Service層のメソッドを呼び出したり、DBにアクセスしたりする必要がある、より複雑なビジネスロジックを含むバリデーションには不向きな場合があります。(ConstraintValidator
をSpring BeanにすればDIも可能ですが、Validator
インターフェースの方がより直接的です)
そのような場合は、Spring独自のValidator
インターフェースが役立ちます。
実装方法
ここでは、「ユーザー名がすでにデータベースに存在するか」というチェックをValidator
インターフェースで実装する例を見ていきましょう。(簡単のため、DBアクセスの部分は擬似的なServiceクラスで代用します)
Step 1: Validator
インターフェースの実装
org.springframework.validation.Validator
を実装したクラスを作成します。
src/main/java/com/example/springvalidatordemo/validator/UserFormValidator.java
“`java
package com.example.springvalidatordemo.validator;
import com.example.springvalidatordemo.form.UserForm;
import com.example.springvalidatordemo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
@Component // DIコンテナに登録
public class UserFormValidator implements Validator {
private final UserService userService;
@Autowired
public UserFormValidator(UserService userService) {
this.userService = userService;
}
@Override
public boolean supports(Class<?> clazz) {
// このValidatorがどのクラスを対象とするかを判定
return UserForm.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
// targetが検証対象のオブジェクト, errorsにエラー情報を格納
UserForm userForm = (UserForm) target;
String name = userForm.getName();
// ユーザー名が既に存在するかをチェック
if (userService.isUsernameExists(name)) {
// エラーを登録
// 第1引数: エラーを関連付けるフィールド名
// 第2引数: エラーコード (messages.propertiesで利用)
// 第3引数: デフォルトのエラーメッセージ
errors.rejectValue("name", "duplicate.userForm.name", "このユーザー名は既に使用されています。");
}
// パスワードの一致チェックもこちらに書くことができる
// String password = userForm.getPassword();
// String confirmPassword = userForm.getConfirmPassword();
// if (password != null && !password.equals(confirmPassword)) {
// errors.rejectValue("confirmPassword", "unmatch.password", "パスワードが一致しません。");
// }
}
}
``
UserService
(は、
isUsernameExists`メソッドを持つダミーのServiceクラスとします)
ポイント:
@Component
: このValidatorをSpringのBeanとしてDIコンテナに登録します。これにより、UserService
などの他のBeanをインジェクションできます。supports(Class<?> clazz)
: このValidatorが引数のclazz
を検証できるかをtrue
/false
で返します。UserForm
クラスまたはそのサブクラスの場合にtrue
を返します。validate(Object target, Errors errors)
: 実際のバリデーションロジックをここに記述します。errors.rejectValue(...)
: フィールドに関連するエラーを登録します。BindingResult
はErrors
インターフェースを継承しているため、ここに登録されたエラーはControllerのBindingResult
で受け取れます。errors.reject(...)
: フィールドに依存しないグローバルなエラー(例: 「入力内容に全体的な矛盾があります」)を登録する場合に使います。
Step 2: Controllerでの利用
このカスタムValidatorをControllerで利用するには、主に2つの方法があります。
方法A: 手動で呼び出す
“`java
@Controller
public class UserController {
@Autowired
private UserFormValidator userFormValidator; // Validatorをインジェクション
// ...
@PostMapping("/user/register")
public String processCreateForm(@Valid @ModelAttribute UserForm userForm, BindingResult bindingResult) {
// まずはBean Validationを実行
if (bindingResult.hasErrors()) {
return "user/registerForm";
}
// 次にカスタムValidatorを手動で実行
userFormValidator.validate(userForm, bindingResult);
if (bindingResult.hasErrors()) {
return "user/registerForm";
}
return "redirect:/user/success";
}
}
“`
この方法は直感的ですが、Validatorの呼び出しを忘れる可能性があります。
方法B: @InitBinder
で自動登録する(推奨)
Controllerに @InitBinder
を付けたメソッドを追加することで、そのControllerで処理されるデータバインディングのプロセスに、カスタムValidatorを自動的に組み込むことができます。
“`java
@Controller
public class UserController {
@Autowired
private UserFormValidator userFormValidator;
// このコントローラーがリクエストを受け取る前に実行される
@InitBinder("userForm") // "userForm" という名前のModel属性に対して適用
public void initBinder(WebDataBinder binder) {
binder.addValidators(userFormValidator);
}
// ...
@PostMapping("/user/register")
public String processCreateForm(@Valid @ModelAttribute("userForm") UserForm userForm, BindingResult bindingResult) {
// @Valid が付いているので、Bean Validation と initBinder で登録した Validator の両方が自動で実行される!
if (bindingResult.hasErrors()) {
return "user/registerForm";
}
return "redirect:/user/success";
}
}
``
@InitBinder
この方法が最もスマートです。により、
@Validが指定された
userFormオブジェクトに対して、標準のBean Validationに加えて
UserFormValidator`も自動的に実行されるようになります。ロジックが分散せず、ControllerのPOSTメソッドはすっきりしたままです。
8. REST APIにおけるバリデーション
ここまでは画面を持つWebアプリケーション(MVC)を例にしましたが、JSONをやり取りするREST APIでもバリデーションは同様に重要です。
@RestController
でのバリデーション
@RestController
でのバリデーションは非常に簡単です。@RequestBody
で受け取るリクエストボディのオブジェクトに@Valid
を付けるだけです。
src/main/java/com/example/springvalidatordemo/api/UserApiController.java
“`java
package com.example.springvalidatordemo.api;
import com.example.springvalidatordemo.form.UserForm;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping(“/api/users”)
public class UserApiController {
@PostMapping
public ResponseEntity<?> createUser(@Valid @RequestBody UserForm userForm) {
// バリデーションが成功した場合のみ、このメソッド内のコードが実行される
// (本来はユーザー作成処理)
return ResponseEntity.ok(Map.of("message", "User created successfully"));
}
}
``
BindingResult`を引数に取りません。では、バリデーションエラーが発生した場合はどうなるのでしょうか?
MVCのControllerと異なり、
@Valid
が付いた@RequestBody
でバリデーションエラーが発生すると、SpringはMethodArgumentNotValidException
という例外をスローします。デフォルトでは、Spring Bootはこの例外をハンドリングし、ステータスコード400 Bad Request
と共に、エラーの詳細情報を含むJSONレスポンスをクライアントに返します。
エラーレスポンスのカスタマイズ
デフォルトのエラーレスポンスは情報量が多いですが、APIの仕様としてレスポンス形式を統一したい場合が多いでしょう。その場合は、@RestControllerAdvice
と@ExceptionHandler
を使って、グローバルな例外ハンドリングを実装します。
src/main/java/com/example/springvalidatordemo/exception/GlobalExceptionHandler.java
“`java
package com.example.springvalidatordemo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
Map<String, Object> body = new HashMap<>();
body.put("status", HttpStatus.BAD_REQUEST.value());
body.put("error", "Validation Failed");
body.put("details", errors);
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}
}
“`
ポイント:
@RestControllerAdvice
: アプリケーション全体の@RestController
を対象とした共通処理を定義するクラスに付与します。AOP(アスペクト指向プログラミング)の一種です。@ExceptionHandler
: 特定の例外が発生した際に実行されるメソッドを定義します。ここではMethodArgumentNotValidException
を捕捉しています。- メソッド内では、例外オブジェクトからエラー情報を抽出し、API仕様に沿った独自のJSONレスポンス形式を構築して返却しています。
これにより、APIのどのエンドポイントでバリデーションエラーが発生しても、一貫したフォーマットでエラーレスポンスを返すことが可能になります。
9. まとめ
この記事では、Spring Frameworkにおけるバリデーションの基礎から応用までを、具体的なサンプルコードと共に詳しく解説しました。
最後に、重要なポイントを振り返りましょう。
- バリデーションは堅牢なアプリケーションの要: データ整合性、セキュリティ、UX向上のために不可欠です。
- Bean Validationを基本に: アノテーションベースのBean Validationは、シンプルで直感的なため、単一フィールドの基本的なチェックの第一選択肢です。
- エラーメッセージはユーザーのために:
messages.properties
を使い、分かりやすい日本語のエラーメッセージを提供しましょう。 - 複雑な要件には応用テクニックを:
- グループ化 (
@Validated
): シナリオに応じてバリデーションルールを切り替える場合に有効です。 - カスタムアノテーション: 独自のバリデーションルールを再利用可能な形で定義できます。相関チェックに便利です。
- グループ化 (
Validator
インターフェースで柔軟性を: ビジネスロジックやDBアクセスを伴う複雑なバリデーションは、Spring独自のValidator
インターフェースでプログラム的に実装します。@InitBinder
との組み合わせが強力です。- REST APIでもバリデーションは必須:
@Valid
と@RequestBody
を組み合わせ、@RestControllerAdvice
でエラーレスポンスを統一的にハンドリングします。
バリデーションは、一見地味な作業に見えるかもしれません。しかし、このプロセスを丁寧に行うことが、最終的にアプリケーション全体の品質を大きく向上させ、ユーザーからの信頼を得るための鍵となります。
Springが提供する強力なバリデーション機能を使いこなし、より信頼性の高い、使いやすいアプリケーション開発を目指してください。
参考資料: