Spring Bootのバリデーション
Spring Boot Webアプリケーションの入力値検証(バリデーション)の設定は、 複数の手順を踏んで設定する必要があるので一見大変そうですが、一つずつやれば難しくはありません。

[1] pom.xmlにvalidationを記述
バリデーションの仕組みを使うためには、pom.xmlに以下の記述が必要です。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

[2] JavaBeanに制約条件を追記
入力内容をチェックしたい編集フォームBeanなどのクラスの各変数に、 @NotBlank、@Size、@Min、@Max などのアノテーションを設定します。

import org.hibernate.validator.constraints.Range;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;

@Data
public class Shiten {
    @NotBlank
    @Size(min=1, max=5)
    private String shitenCode;

    @NotBlank
    @Size(min=1, max=10)
    private String shitenName;

    @NotBlank
    private String address;

    @NotNull
    @Range(min=1, max=9)
    private Integer areaCode;

    @NotNull
    @Min(2)
    @Max(999)
    private Integer employeeNum;
}

@NotNull、@NotEmpty、@NotBlankというよく似た3種類のチェックがあります。 違いは、
  1. @NotNullはnullのみエラーで、空文字列や空白文字だけからなる文字列は許可する
  2. @NotEmptyはnullと空文字列がエラー、空白文字だけからなる文字列は許可する
  3. @NotBlankは全てエラー
大きな注意点として、Web画面のフォームで入力された文字列は空欄だとnullではなく必ず空文字列になるので、 変数の値がnullになることはありません。 したがって、一般的なバリデーションの処理では、文字列変数に@NotNullを使用しても意味がありません。 @NotEmptyか@NotBlankを使うことになります。
一方、Integer型の変数には@NotBlank等は指定できません(ビルドはできますが、チェック実行時にエラーになります)。 数値型プロパティの必須チェックには@NotNullを使うことになります。
それに関して二つ目の注意点として、@Minや@Maxを指定すると数値の入力範囲を指定できますが、 これらを指定しても空欄はエラーにならず通ってしまいます。 必須入力にしたければ、@Minや@Maxと共に@NotNullも必要です。

別の書き方の例を挙げましょう。

    @Pattern(regexp = "^\\d*$", message = "{0}は整数を入力してください。")
    @Size(min = 0, max = 3)
    //@Range(min = 0, max = 999)
    private String employeeNumMin, employeeNumMax;

上記のように、message=でエラーメッセージを直接Javaプログラム中に書くことも可能です。
また@Sizeは入力文字数を、@Rangeは入力値の範囲を設定する物ですが、 @Sizeはmin=1以上の値を指定するとそれは必須入力の意味になります。min=0ならば空欄(0文字)も許可されます。 @Rangeの場合は、minの値を問わず指定してしまったら常に必須入力になります。

[3] ValidationMessages.properties を作る(オプション)
src/main/resources ディレクトリにValidationMessages.properties を作って配置します。 (別の名前でも全く構いませんが、慣習的にこの名がよく使われるようです)

jakarta.validation.constraints.NotBlank.message={0}は必須入力です。
jakarta.validation.constraints.NotNull.message={0}は必須入力です。
jakarta.validation.constraints.Min.message={0}には{1}以上の値を入力してください。
jakarta.validation.constraints.Max.message={0}には{1}以下の値を入力してください。
jakarta.validation.constraints.Size.message={0}は{2}〜{1}文字の範囲で入力してください。
org.hibernate.validator.constraints.Range.message={0}の値は{2}〜{1}の範囲で入力してください。

※実際には一般的なpropertiesファイルと同様、Unicodeエスケープが必要です。
{0}、{1}などの「プレースホルダー」は、エラーメッセージ表示時に各チェック内容ごとに実際の値に置き換えられます。
{0}はほとんどの場合、message.propertiesで定義した項目名称が入ります。 {1}以降は、例えばMinならば「{0}には{1}以下の値を入力してください。」のように使われます。
なおこのファイルには上のように、品目コードとか在庫数量などのアプリケーション固有の項目名は含めず、 複数のアプリケーションで再利用できるメッセージだけを定義するのが普通です。

ちなみに、SizeとRangeの定義が何か変じゃないかって?これで正しいみたいです。
つまり、{1}が最大値、{2}が最小値です。何でそうなんだと言ってもそうなんだから仕方がない(私も聞きたいです)。
なお、インターネットでは{1} {2}の代わりに{min} {max}のような表記が可能との情報もありますが、私が試すとエラーになり、動作しませんでした。

[4] messages.properties を作る
バリデーションの対象となる入力項目、つまりBean変数ごとに、 変数名とそれがエラーメッセージ内で表示される際の項目名称の対応を記述した messages.propertiesというファイルを作成して、src/main/resources ディレクトリに配置します。

shitenCode=支店コード
shitenName=支店名称
areaCode=地域コード
address=所在地
employeeNum=従業員数

※実際には一般的なpropertiesファイルと同様、Unicodeエスケープが必要です。
で、このファイルの中にアプリケーション固有のエラーメッセージを記述すると、 ValidationMessages.propertiesよりもこちらの内容が優先されます。

shitenCode=支店コード
shitenName=支店名称
areaCode=地域コード
address=所在地
employeeNum=従業員数

NotBlank={0}は必須入力です。

NotNull.shiten.areaCode={0}は必須選択です。

# 下記は上の物ほど優先される
Min.shiten.employeeNum={0}には{1}以上の値を入力してください。(1)
Min.employeeNum={0}には{1}以上の値を入力してください。(2)
Min.java.lang.Integer={0}には{1}以上の値を入力してください。(3)
Min={0}には{1}以上の値を入力してください。(4)

Range={0}には{2}〜{1}の範囲の値を入力してください。
Size={0}の内容は{2}〜{1}文字の範囲で入力してください。

typeMismatch.java.lang.Integer={0}は整数で入力してください。

例えば、このアプリケーションでは項目areaCodeはキー入力ではなくプルダウン選択を想定しているので、 「必須入力」の代わりに「必須選択」としています。
一方でよーく見ると、項目employeeNumに対するNotNullのメッセージがありません。 このファイルに書いていない場合は、ValidationMessages.properties の記述の方が採用されます。
また同じチェック内容でも、対象を限定するほど優先されます。 上の例では「Min.{オブジェクト変数名}.{項目名}」つまり「どのフォームのどの項目」 まで指定したメッセージが最優先となり、以下、オブジェクトを省略した場合、データ型を省略した場合、 全て省略して「Min」とだけ書いた場合の優先順位になります。
また、最後の「typeMismatch.java.lang.Integer」は、 Beanのデータ型がString以外の変数に対して、そのデータ型に変換できないエラー (例、Integer型の変数に"ZZZ"が入力された場合など)に表示されるメッセージを指定します。
エラーメッセージの再利用性を考慮しないなら、 NotBlankとかMinとかの汎用エラーメッセージも全て messages.properties に書くことも可能です。 その場合は、ValidationMessages.properties を作る必要はなく、 次の application.properties の記述も不要です。

[5] application.propertiesの編集 (オプション)
他の処理系と混同して私は途中まで勘違いしていたのですけど、 Spring Boot 3 ではデフォルトで読まれるのは messages.properties だけで、 明示的に指定しない限り ValidationMessages.properties の内容は有効になりません。 有効にするには、application.properties に以下のように記述します。

(前略)
spring.messages.basename=messages, ValidationMessages
spring.messages.cache-duration=-1

spring.messages.basenameは複数行書けないので、複数のファイルを指定する場合カンマで区切ります。 ファイル名は.propertiesは不要で、src/main/resourcesからの相対パスを指定します。
おまけに付けた spring.messages.cache-duration はオプションで、 読んだリソースをキャッシュする期間です。単位を省略した際のデフォルトは秒。 -1はキャッシュしない設定なので開発環境で使うことになりましょう。 なお 公式リファレンスによると、 指定しない場合は永久に、つまりアプリケーションが停止するまでキャッシュし続けるそうです。

[6] Thymeleafテンプレートにエラー発生時のメッセージ表示を追記
エラーメッセージは、1個所に全入力項目のメッセージをまとめて表示することも、 項目ごとに入力欄の傍に個別に表示することもできます。下記の例は後者です。 shitenNameという入力欄の傍に、shitenNameに関する入力エラー項目だけを表示します。

支店名称
<input type="text" th:field="*{shitenName}">
<span th:if="${#fields.hasErrors('shitenName')}" th:errors="*{shitenName}" style="color:red" />

次に前者の例。1個所に全入力項目のメッセージをまとめて表示する場合は次のように書きます。

<div th:if="${#fields.hasAnyErrors()}">
  <ul>
    <li th:each="err : ${#fields.allErrors()}" th:text="${err}" style="color:red"/>
  </ul>
</div>

[7] Controllerにエラーチェックを追記

@RequestMapping("/STNP026Save")
public String save(@ModelAttribute("shiten") @Valid Shiten shiten, BindingResult bindingResult, Model model) {
    if (bindingResult.hasErrors()) {
        return "editpage";
    }
    int rows = service.updateByPrimaryKey(shiten);
    model.addAttribute("message", rows + "行のデータを更新しました。");
    model.addAttribute("shiten", null);
    return "editpage";
}

入力内容を受け取るメソッドに、バリデーションを行うモデルオブジェクトに@Validを付けること、 BindingResultを引数に渡すこと、制約違反を検知してreturn処理を行うこと、の3つを追加します。
なお、@Validと@Validatedというよく似たアノテーションがあります。
前者はjakarta.validation由来、後者はspring由来で、機能もよく似ていますが、 チェックの優先度をグループで付けられる機能を使う場合は@Validatedを使う必要があります。
重大な注意が一つ。BindingResultの引数は、必ず@Validを付けた引数の直後に置く必要があります。
他の位置に書いた場合、ExceptionHandlerには「400 BAD REQUEST」が、 ログにはMethodArgumentNotValidExceptionという物が出てしまいます。私は原因が分からず延々悩みました。


List型プロパティ変数のバリデーション
編集フォームのBean内部にListのようなプロパティ変数を持っている場合、 その各要素に対して、RecordBeanクラスで定義したバリデーションを効かせるには? 答えは次のようにList型変数に@Validというアノテーションを付けることです。

@Data
public class EditForm {
    @Valid
    private List recordList = new ArrayList<RecordBean>();
}

これでこのようなチェックが効くようになります。

@Data
public class RecordBean {
    @NotBlank
    private String orderNumber;
}

ちなみにこの場合、バリデーションエラーがあると、標準では次のようなメッセージになってしまいます。

recordList[0].orderNumberは必須入力です。

いいのか悪いのか分かりませんが、やっつけだとこのようにmessages.properties に定義を並べるといい感じのメッセージになります。良い方法は調査中です。

recordList[0].orderNumber=注文番号1
recordList[1].orderNumber=注文番号2
# ...

Java kowaza Top

(first uploaded 2024/08/03 last updated 2025/04/06, URANO398)