Spring Validator入門:サンプルコードで学ぶバリデーションの基礎


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つのアプローチを組み合わせる「ハイブリッドアプローチ」です。

  1. 単一フィールドの基本的なチェック(必須チェック、文字数、フォーマットなど)は、Bean Validationのアノテーションで行います。これにより、コードの大部分をシンプルで読みやすく保てます。
  2. Bean Validationだけでは実現できない複雑な相関チェックやビジネスロジックが必要な場合に限り、Validatorインターフェースカスタムバリデーションアノテーションで補います。

この記事では、まず最も一般的なBean Validationから詳しく解説し、その後でValidatorインターフェースを使ったプログラム的な実装方法についても掘り下げていきます。

3. 環境構築

それでは、実際に手を動かしながら学ぶために、Spring Bootプロジェクトを準備しましょう。

Spring Bootプロジェクトの作成

start.spring.io を利用するのが最も簡単です。

  1. https://start.spring.io/ にアクセスします。
  2. 以下の項目を設定します。
    • 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
    • Dependencies:
      • Spring Web
      • Thymeleaf (画面表示用)
      • Validation
  3. GENERATE ボタンをクリックし、zipファイルをダウンロードします。
  4. ダウンロードしたzipファイルを解凍し、お好みのIDE(IntelliJ IDEA, Eclipse, VS Codeなど)でプロジェクトを開きます。

依存関係の確認

Mavenを使用する場合、pom.xml に以下の依存関係が含まれていることを確認してください。

pom.xml
“`xml


org.springframework.boot
spring-boot-starter-thymeleaf


org.springframework.boot
spring-boot-starter-validation


org.springframework.boot
spring-boot-starter-web

<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";
}

}
“`
重要なポイント:

  1. @Valid アノテーション: バリデーションを実行したいメソッド引数(ここではUserForm)の前に @Valid を付与します。これにより、Spring MVCはUserFormに設定されたアノテーションに基づいてバリデーションを自動的に実行します。
  2. BindingResult 引数: @Valid を付けた引数の 直後BindingResult 型の引数を置きます。このオブジェクトにバリデーションの結果(エラー情報)が格納されます。BindingResult を引数に取ることで、バリデーションエラーが発生しても例外がスローされず、コントローラー内でエラー処理を続行できます。
  3. bindingResult.hasErrors(): このメソッドでバリデーションエラーの有無を判定します。エラーがあれば true を返し、フォーム画面(user/registerForm)を再表示します。この時、userFormオブジェクトにはユーザーが入力した値とエラー情報が保持されたままなので、入力内容を維持した状態でエラーメッセージを表示できます。
  4. @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





ユーザー登録


ユーザー登録フォーム


Name Error


Age Error


Email Error


Website Error



“`

src/main/resources/templates/user/success.html
“`html





登録完了

ユーザー登録が完了しました!

戻る

“`

Thymeleafの重要なポイント:

  • th:object="${userForm}": <form> タグで、コントローラーから渡されたuserFormオブジェクトをバインドします。
  • th:field="*{fieldName}":<input>タグで、th:objectで指定したオブジェクトのプロパティ(フィールド)に紐付けます。*{...}th:object への相対パスを示します。th:fieldid, 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は、以下の順序で適用するメッセージキーを探します。優先度が高い順に並んでいます。

  1. Annotation.objectName.fieldName (例: NotBlank.userForm.name)
  2. Annotation.fieldName (例: Min.age)
  3. Annotation.fieldType (例: NotNull.java.lang.Integer)
  4. 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(...): フィールドに関連するエラーを登録します。BindingResultErrorsインターフェースを継承しているため、ここに登録されたエラーは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"));
}

}
``
MVCのControllerと異なり、
BindingResult`を引数に取りません。では、バリデーションエラーが発生した場合はどうなるのでしょうか?

@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が提供する強力なバリデーション機能を使いこなし、より信頼性の高い、使いやすいアプリケーション開発を目指してください。

参考資料:

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール