SpringMVCのバリデーションで「各項目1エラーずつ・優先順位付き」を標準アノテーションだけで実現する
はじめに
SpringMVCでフォームバリデーションを実装する際、こんな要件に悩んだことはありませんか?
- 1つの項目に複数のエラーがある場合、最初のエラーだけを表示したい
- エラーの優先順位を守りたい(必須→文字種→桁数の順)
- 各項目ごとに独立してチェックしたい
例えば、数値コード入力欄で:
- 未入力なら「必須です」
- 文字が混ざっていたら「数字のみで入力してください」
- 桁数が違ったら「10桁で入力してください」
と、1つずつ順番にエラーを出したいケースです。
(要求仕様や現行踏襲で「数字10桁の入力が必須です」と一つのチェックにまとめられないケース)
よくある解決策とその限界
1. GroupSequenceを使う方法
public interface First {}
public interface Second {}
public interface Third {}
@GroupSequence({First.class, Second.class, Third.class})
public interface ValidationSequence {}
public class MyForm {
@NotBlank(groups = First.class, message = "コードは必須です")
@Pattern(regexp = "^[0-9]+$", groups = Second.class, message = "数字のみです")
@Size(min = 10, max = 10, groups = Third.class, message = "10桁です")
private String code;
@NotBlank(groups = First.class, message = "名前は必須です")
@Size(max = 50, groups = Second.class, message = "50文字以内です")
private String name;
}
問題点:
@GroupSequenceはDTO全体で順序制御されます。つまり:
-
codeがFirstで失敗すると、nameのSecondグループは実行されない - 各項目ごとに独立してチェックできない
2. カスタムValidatorを作る方法
@CodeValidation(message = "コードエラー")
private String code;
public class CodeValidator implements ConstraintValidator<CodeValidation, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isBlank()) {
context.buildConstraintViolationWithTemplate("必須です").addConstraintViolation();
return false;
}
if (!value.matches("^[0-9]+$")) {
context.buildConstraintViolationWithTemplate("数字のみです").addConstraintViolation();
return false;
}
// ...以下続く
}
}
問題点:
- 項目ごとにValidatorクラスが必要で冗長
- コード量が増える
3. BindingResultで後処理する方法
@PostMapping
public String submit(@Valid MyForm form, BindingResult result) {
if (result.hasErrors()) {
// 各項目の最初のエラーだけ取り出す処理
Map<String, String> errors = new HashMap<>();
for (FieldError error : result.getFieldErrors()) {
errors.putIfAbsent(error.getField(), error.getDefaultMessage());
}
// ...
}
}
問題点:
- 全エラーがチェックされてしまう(優先順位が効かない)
- Controller側で毎回同じ処理を書く必要がある
解決策:正規表現による「委譲パターン」
標準アノテーションだけで実現する方法を考えました。
public class MyForm {
@NotBlank(message = "コードは必須です")
@Pattern(regexp = "^[0-9]*$", message = "コードは数字のみで入力してください")
@Pattern(regexp = "^$|^.*[^0-9]+.*$|^[0-9]{10}$", message = "コードは10桁で入力してください")
private String code;
}
仕組みの解説
ポイントは3番目の@Patternの正規表現です:
^$|^.*[^0-9]+.*$|^[0-9]{10}$
この正規表現は以下の3パターンにマッチします:
-
^$→ 空文字はOK(@NotBlankが担当するから) -
^.*[^0-9]+.*$→ 数字以外を含む文字列はOK(2番目の@Patternが担当するから) -
^[0-9]{10}$→ 10桁の数字のみOK(自分の担当)
つまり、「前段のチェックで弾かれるケースは意図的に通す」ことで、自分の担当(桁数チェック)だけに集中できるようにしています。
動作確認
パターン1:空文字を入力
-
@NotBlank→ エラー -
@Pattern("^[0-9]*$")→ OK(*は0文字以上にマッチ) -
@Pattern("^$|...")→ OK(^$にマッチ)
結果:「コードは必須です」のみ表示
パターン2:「abc」を入力
-
@NotBlank→ OK -
@Pattern("^[0-9]*$")→ エラー -
@Pattern("^$|...")→ OK(^.*[^0-9]+.*$にマッチ)
結果:「コードは数字のみで入力してください」のみ表示
パターン3:「123」を入力
-
@NotBlank→ OK -
@Pattern("^[0-9]*$")→ OK -
@Pattern("^$|...")→ エラー(どのパターンにもマッチしない)
結果:「コードは10桁で入力してください」のみ表示
パターン4:「1234567890」を入力
-
@NotBlank→ OK -
@Pattern("^[0-9]*$")→ OK -
@Pattern("^$|...")→ OK(^[0-9]{10}$にマッチ)
結果:エラーなし
| 入力値 | @NotBlank |
@Pattern(数字) |
@Pattern(10桁) |
表示されるエラー |
|---|---|---|---|---|
| (空文字) | ❌ | ✅ | ✅ | コードは必須です |
| "abc" | ✅ | ❌ | ✅ | コードは数字のみで入力してください |
| "123" | ✅ | ✅ | ❌ | コードは10桁で入力してください |
| "1234567890" | ✅ | ✅ | ✅ | (エラーなし) |
メリット
- ✅ 標準アノテーションのみで実装可能
- ✅ カスタムValidatorが不要
- ✅ 各項目ごとに独立してチェックできる
- ✅ チェックロジックがDTOに集約される
- ✅ 優先順位が確実に守られる
デメリット
- ⚠️ 正規表現が複雑になる
- ⚠️ チェック項目が増えると設計が難しくなる
- ⚠️ メンテナンスする人が意図を理解する必要がある
優先順位を変更する場合
この手法の良いところは、正規表現を組み替えるだけで優先順位を変更できる点です。
例:必須 → 桁数 → 文字種の順にチェックしたい場合
@NotBlank(message = "コードは必須です")
@Pattern(regexp = "^$|^.{10}$", message = "コードは10桁で入力してください")
@Pattern(regexp = "^.{0,9}$|^.{11,}$|^[0-9]*$", message = "コードは数字のみで入力してください")
private String code;
2番目の正規表現(桁数チェック):
-
^$→ 空文字OK(@NotBlankが担当) -
^.{10}$→ 10桁ならOK(自分の担当)
3番目の正規表現(文字種チェック):
-
^.{0,9}$→ 9桁以下OK(2番目が担当) -
^.{11,}$→ 11桁以上OK(2番目が担当) -
^[0-9]*$→ 数字のみOK(自分の担当)
| 入力値 | @NotBlank |
@Pattern(桁数) |
@Pattern(文字種) |
表示されるエラー |
|---|---|---|---|---|
| (空文字) | ❌ | ✅ | ✅ | コードは必須です |
| "abc" | ✅ | ❌ | ✅ | コードは10桁で入力してください |
| "abcdefghij" | ✅ | ✅ | ❌ | コードは数字のみで入力してください |
| "123" | ✅ | ❌ | ✅ | コードは10桁で入力してください |
| "1234567890" | ✅ | ✅ | ✅ | (エラーなし) |
複数項目での使用例
public class UserForm {
@NotBlank(message = "ユーザーIDは必須です")
@Pattern(regexp = "^[a-zA-Z0-9]*$", message = "ユーザーIDは半角英数字のみです")
@Pattern(regexp = "^$|^.*[^a-zA-Z0-9]+.*$|^[a-zA-Z0-9]{6,20}$",
message = "ユーザーIDは6文字から20文字です")
private String userId;
@NotBlank(message = "パスワードは必須です")
@Pattern(regexp = "^$|^.{8,}$", message = "パスワードは8文字以上です")
private String password;
@NotBlank(message = "メールアドレスは必須です")
@Email(message = "メールアドレスの形式が正しくありません")
private String email;
}
各項目が独立してチェックされるため、例えば:
-
userIdが空 -
passwordが「123」(短すぎ) -
emailが「test」(形式不正)
という状態でも、それぞれ:
- 「ユーザーIDは必須です」
- 「パスワードは8文字以上です」
- 「メールアドレスの形式が正しくありません」
と、各項目の最初のエラーのみが表示されます。
注意点
正規表現の設計が重要
この手法は「前段のチェックで弾かれるケースを網羅的に列挙する」必要があります。
もし漏れがあると、意図しないエラーが複数出てしまう可能性があります。
複雑なチェックには向かない
チェックロジックが複雑な場合(例:郵便番号と住所の整合性チェック)は、
素直にカスタムValidatorを使った方が良いでしょう。
まとめ
SpringMVCのバリデーションで「各項目1エラーずつ・優先順位付き」を実現する、
標準アノテーションだけを使った実装パターンを紹介しました。
正規表現の設計が少し複雑になりますが、
カスタムValidatorを量産するよりはシンプルに実装できると思います。
同じような課題で悩んでいる方の参考になれば幸いです。