0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SpringMVCのバリデーションで「各項目1エラーずつ・優先順位付き」を標準アノテーションだけで実現する

0
Last updated at Posted at 2026-01-23

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パターンにマッチします:

  1. ^$空文字はOK@NotBlankが担当するから)
  2. ^.*[^0-9]+.*$数字以外を含む文字列はOK(2番目の@Patternが担当するから)
  3. ^[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を量産するよりはシンプルに実装できると思います。

同じような課題で悩んでいる方の参考になれば幸いです。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?