@Valid は付けられるようになったけれど、「この文字列には @NotEmpty@NotBlank のどっち?」「LocalDate に使える制約は?」「日本語メッセージはどこに書く?」と、毎回リファレンスを行き来していませんか。

この記事は、Spring Boot 3.x で使える Bean Validation の制約アノテーション(validation annotations)を 一覧表 にまとめ、迷いやすい使い分けと ValidationMessages.properties によるメッセージ上書きだけに絞ったリファレンスです。@Valid の仕組みやエラーハンドリングは @Validの使い方の記事 に任せて、すぐ表に進みましょう。

前提は Spring Boot 3.x、Jakarta Bean Validation 3.0、Hibernate Validator 8 系です。Spring Boot 3 からパッケージが javax.validation から jakarta.validation に変わっているので、古い記事のコピペには注意してください。依存は次の 1 行です(Maven なら同じ artifactIddependency に追加します)。

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'

標準制約アノテーション一覧表(早見表)

jakarta.validation.constraints に入っている標準制約は次の 22 個です。メッセージは英語のデフォルト文の意訳です。

アノテーション対象型nullのとき主な属性デフォルトメッセージ
@NotNull任意NG-nullは許可されていません
@Null任意OK(null必須)-nullでなければなりません
@NotEmpty文字列, Collection, Map, 配列NG-空であってはいけません
@NotBlank文字列(CharSequence)NG-空白であってはいけません
@Size文字列, Collection, Map, 配列OKmin, maxサイズは{min}から{max}の間でなければなりません
@Min整数系, BigDecimal, BigIntegerOKvalue{value}以上でなければなりません
@Max同上OKvalue{value}以下でなければなりません
@DecimalMin上記 + 文字列OKvalue, inclusive{value}以上(またはより大きく)なければなりません
@DecimalMax上記 + 文字列OKvalue, inclusive{value}以下(または未満)でなければなりません
@Positive整数系, BigDecimal, BigIntegerOK-0より大きくなければなりません
@PositiveOrZero同上OK-0以上でなければなりません
@Negative同上OK-0より小さくなければなりません
@NegativeOrZero同上OK-0以下でなければなりません
@Digits数値, 文字列OKinteger, fraction数値の範囲外です(整数{integer}桁, 小数{fraction}桁)
@Past日時型OK-過去の日付でなければなりません
@PastOrPresent日時型OK-過去または現在の日付でなければなりません
@Future日時型OK-未来の日付でなければなりません
@FutureOrPresent日時型OK-現在または未来の日付でなければなりません
@Email文字列OKregexp, flags電子メールアドレスとして正しくありません
@Pattern文字列OKregexp, flags”{regexp}“と一致させてください
@AssertTrueboolean, BooleanOK-trueでなければなりません
@AssertFalseboolean, BooleanOK-falseでなければなりません

「整数系」は byte/short/int/long とそのラッパーです。@Min/@Max@Positive 系はどちらも double/float が仕様上サポート外なので、小数は BigDecimal にしてください(理由は数値の節で説明します)。

表を見て気づくと思いますが null を弾くのは NotNull・NotEmpty・NotBlank の 3 つだけ です。それ以外の制約はすべて null を「検証対象外」として合格させます。@Size(max = 20) だけ付けたフィールドに null が来ても素通りするので、必須にしたいなら必ず @NotNull 系を併用してください。ここが一番多いハマりどころです。

以降の節は、この表の分類ごとの補足です。

@NotNull・@NotEmpty・@NotBlankの違い

一番よく聞かれる 3 つの違いは、型と入力値のマトリクスで見ると一発です。

入力値@NotNull@NotEmpty@NotBlank
nullNGNGNG
""(空文字)OKNGNG
" "(空白のみ)OKOKNG
"abc"OKOKOK
空の List / Map / 配列OKNG例外
要素ありの ListOKOK例外

@NotBlankCharSequence 専用です。ListInteger に付けると、検証時に UnexpectedTypeException(HV000030)が投げられます。「Integer の必須チェックに @NotBlank を付けたら 500 エラーになった」というのは定番のトラブルです。

public record UserRequest(
    @NotBlank String name,              // 空白だけの名前も弾く
    @NotEmpty List<String> roles,       // 空リストは弾く、要素の中身は見ない
    @NotNull Integer age,               // 数値の必須は @NotNull
    // @NotBlank Integer age            // これは UnexpectedTypeException
    String nickname                     // 任意項目は何も付けない
) {}

使い分けはシンプルで、ユーザーが入力する文字列は @NotBlank、リストや Map の必須は @NotEmpty、数値・日時・Boolean などの参照型は @NotNull と覚えておけば十分です。

@Sizeと@Lengthの違い(長さ制約)

@Size は標準制約で、文字列だけでなく CollectionMap・配列の要素数にも使えます。一方 @Lengthorg.hibernate.validator.constraints にある Hibernate Validator 独自の制約で、文字列専用です。できることは @Size に含まれるので、基本は @Size を選びましょう。

public record ArticleRequest(
    @NotBlank @Size(max = 100) String title,               // 必須かつ100文字以内
    @NotEmpty @Size(max = 5) List<@NotBlank String> tags   // 1〜5件、各要素も空白不可
) {}

List<@NotBlank String> のように型引数に制約を書くと、要素そのものを検証できます。@Size はあくまで「件数」の制約なので、混同しないようにしてください。

数値に使う制約 @Min/@Max・@DecimalMin/@DecimalMax・@Positive系・@Digits

@Min/@Maxlong で境界を指定し、byte/short/int/long(とラッパー)、BigDecimalBigInteger に対応します。double/float は丸め誤差の理由で 仕様上はサポート外 です。Hibernate Validator では動いてしまうのですが、金額のような小数を扱うなら BigDecimal にして @DecimalMin を使うのが安全です。

@DecimalMin/@DecimalMax は境界を文字列で書くので "0.01" のような小数も指定でき、inclusive = false にすると境界値自体を弾けます。符号だけ見たいなら @Positive 系、桁数を縛りたいなら @Digits です。

public record OrderRequest(
    @NotNull @Min(1) @Max(999) Integer quantity,
    @NotNull @DecimalMin(value = "0", inclusive = false)   // 0より大きい
    @Digits(integer = 8, fraction = 2) BigDecimal amount,  // 99999999.99まで
    @PositiveOrZero Integer point                          // 任意項目、指定時は0以上
) {}

数値の必須チェックはプリミティブ型ではなくラッパー型にしましょう。int quantity だとリクエストに項目が無くても 0 が入るため、@NotNull が意味を持ちません。

日時に使う制約 @Past/@Future系とjava.time

日時には 4 つの制約があり、違いは「現在」を含むかどうかだけです。

  • @Past は現在より前、@PastOrPresent は現在を含む過去
  • @Future は現在より後、@FutureOrPresent は現在を含む未来

対象型は LocalDate/LocalDateTime/LocalTime/Instant/ZonedDateTime/OffsetDateTime/Year/YearMonth/MonthDay などの java.time 一式と、java.util.Date/Calendar です。

public record ReservationRequest(
    @NotNull @Past LocalDate birthDate,               // 生年月日は過去のみ
    @NotNull @FutureOrPresent LocalDateTime visitAt   // 来店日時は今以降
) {}

こちらも null は合格扱いなので、必須なら @NotNull を忘れずに付けてください。

メールアドレスと正規表現 @Emailと@Pattern

@Email は Bean Validation 2.0 で標準入りしました。かつて org.hibernate.validator.constraints にあった独自版の @Email/@NotEmpty/@NotBlank は Hibernate Validator 7 で削除されているので、Spring Boot 3.x では jakarta.validation.constraints の標準版だけを使えば大丈夫です。判定はかなり緩く、a@b のような値も通ります。そして重要なのは Email は空文字を合格扱いにする ことです。必須項目なら @NotBlank を必ず併用してください。

正規表現で形式を縛るなら @Pattern です。flags で大文字小文字の無視なども指定できます。

public record ContactRequest(
    @NotBlank @Email String email,
    @Pattern(regexp = "\\d{3}-\\d{4}") String postalCode,   // 123-4567
    @Pattern(regexp = "[a-z0-9_]{4,16}",
             flags = Pattern.Flag.CASE_INSENSITIVE) String loginId
) {}

正規表現が複雑になってきたら、意図が伝わる名前を付けて カスタムバリデーションアノテーション に切り出す方が保守しやすいです。

真偽値に使う制約 @AssertTrue/@AssertFalse

対象は boolean/Boolean だけで、利用規約への同意チェックボックスが典型的な用途です。Boolean が null のときは @AssertTrue も合格してしまうので、同意を必須にするなら @NotNull を併用します。ちょっとした小技として、isXxx() メソッドに付けると複数フィールドの相関チェックを簡易的に書けます。

public record PeriodRequest(
    @NotNull @AssertTrue(message = "利用規約に同意してください") Boolean agreed,
    LocalDate from,
    LocalDate to
) {
    @AssertTrue(message = "終了日は開始日以降にしてください")
    public boolean isValidPeriod() {
        return from == null || to == null || !to.isBefore(from);
    }
}

isValidPeriod() は Jackson から validPeriod プロパティに見えるので、この record をレスポンスにも使い回すなら @JsonIgnore を付けておきましょう。

Hibernate Validator独自の制約一覧

Spring Boot のデフォルト実装は Hibernate Validator なので、独自制約も普通に使えます。ただしパッケージが org.hibernate.validator.constraints になる点に注意してください。標準制約と同名のものは残っていないので、標準側は jakarta.validation.constraints、独自側は org.hibernate.validator.constraints と覚えておけば import で迷いません。

アノテーション対象型用途
@Length(min, max)文字列文字数制限(@Size で代替可)
@Range(min, max)数値, 文字列@Min + @Max の短縮形
@URL文字列URL形式(protocol/host/port も指定可)
@UniqueElementsCollection要素の重複チェック
@CreditCardNumber文字列クレジットカード番号(Luhn)
@LuhnCheck文字列汎用のLuhnチェック
@ISBN文字列ISBN-10/13
@EAN文字列EAN-8/13
@CodePointLength文字列サロゲートペアを1文字として数える長さ制限
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;
import org.hibernate.validator.constraints.UniqueElements;

public record ProfileRequest(
    @URL String website,
    @Range(min = 0, max = 150) Integer age,
    @UniqueElements List<String> skills
) {}

独自制約を使うと Hibernate Validator への依存が生まれますが、Spring Boot で別実装に乗り換えることはまず無いので、実務では気にしなくて大丈夫です。

ネストしたオブジェクトやListの要素には@Validでカスケード

一覧表だけ見ていると忘れがちですが、ネストした DTO や List<DTO> の中身は、@Valid を付けないと評価されません。

public record OrderRequest(
    @NotNull @Valid CustomerRequest customer,
    @NotEmpty List<@Valid ItemRequest> items
) {}

@Valid の動作や BindingResultMethodArgumentNotValidException の扱いは @Validの使い方の記事エラーレスポンス統一の記事 を見てください。グループ指定やメソッド検証は @Validatedの記事 にまとめています。

エラーメッセージをValidationMessages.propertiesで上書きする

メッセージの差し替え先は src/main/resources/ValidationMessages.properties です。クラスパス直下に置くだけで Hibernate Validator が自動で読み込みます。

標準メッセージは {jakarta.validation.constraints.制約名.message} というキーで定義されているので、同じキーを書けば一括で上書きできます。@Size は文字列以外にも効くので、文言は「文字」に限定しない方が無難です。あわせて独自キーも定義しておきましょう。

# 標準メッセージの一括上書き
jakarta.validation.constraints.NotNull.message=必須項目です
jakarta.validation.constraints.NotBlank.message=入力してください
jakarta.validation.constraints.NotEmpty.message=1件以上指定してください
jakarta.validation.constraints.Size.message={min}〜{max}の範囲で指定してください
jakarta.validation.constraints.Min.message={value}以上の値を入力してください
jakarta.validation.constraints.Email.message=メールアドレスの形式が正しくありません
jakarta.validation.constraints.Pattern.message=形式が正しくありません

# 独自キー
user.name.required=ユーザー名は必須です

フィールド個別に変えたいときは message 属性に書きます。{min}/{max}/{value}/{regexp} のようにアノテーションの属性を波括弧で参照でき、${validatedValue} で入力値そのものも埋め込めます。

public record UserRequest(
    @NotBlank(message = "{user.name.required}")             // プロパティのキーを参照
    @Size(min = 2, max = 20, message = "{min}〜{max}文字で入力してください")
    String name,

    @Min(value = 18, message = "${validatedValue}歳は登録できません({value}歳以上)")
    Integer age
) {}

${validatedValue} はメールアドレスや氏名のような個人情報をログやレスポンスに載せてしまうこともあるので、使う場所は選んでください。

多言語化して Accept-Language で出し分けたい場合は、ValidationMessages_en.properties を追加するか messages_en.properties に寄せます。詳しくは i18n の記事 を参照してください。@ConfigurationProperties の検証は 設定値のバリデーション記事 で扱っています。

日本語メッセージの文字化けを防ぐ設定

昔の記事には native2ascii\u30e6... に変換する手順が載っていますが、Java 9 以降はプロパティファイルをデフォルトで UTF-8 として読むので、ValidationMessages.properties に日本語をそのまま書いて問題ありません。それでも文字化けするときは、次の 3 点を疑ってください。

  • IDE やエディタの保存エンコーディングが Shift_JIS(MS932)になっていないか
  • message 属性に日本語を直接書いている場合、Java ソースのコンパイル時エンコーディング
  • messages.propertiesValidationMessages.properties のどちらに書いたか、そしてキーが一致しているか

2 点目は Java 18 以降なら UTF-8 がデフォルトですが、Java 17 の Windows 環境では明示しておくと安心です。Gradle は compileJava.options.encoding = 'UTF-8'、Maven は project.build.sourceEncodingUTF-8 にします(Spring Boot の parent POM なら設定済みです)。

3 点目について補足します。Spring Boot 2.6 以降(3.x を含む)では、自動設定される Validator が最初から MessageSource と連携しています。{user.name.required}{jakarta.validation.constraints.NotBlank.message} のような {キー} は、まず spring.messages.basename で指定したファイル(既定は messages.properties)で解決され、見つからなければ ValidationMessages.properties と Hibernate Validator 内蔵のメッセージにフォールバックします。つまり、Bean Validation 専用にメッセージを分けたいなら ValidationMessages.properties、アプリの他のメッセージと一元化したいなら messages.properties、という選択になります。messages.properties 側のエンコーディングは spring.messages.encoding(既定 UTF-8)に従うので、こちらも日本語を直接書いて問題ありません。

なお、古い記事にある LocalValidatorFactoryBean を自前で定義して setValidationMessageSource を設定する方法は、Spring Boot 3.x では不要です。自前の Validator Bean を定義すると Boot の自動設定が引っ込み、MessageInterpolatorFactory によるフォールバックの仕組みが外れて ValidationMessages.properties が読まれなくなります。ValidationMessages.properties を完全に MessageSource へ切り替えたい場合の選択肢と考えて、特別な理由が無ければ触らないでおきましょう。

まとめ フィールド型別の制約選択チャート

最後に、型から逆引きできる形にしておきます。

フィールドの型まず付ける制約
String(必須)@NotBlank + @Size(max)
String(形式あり)@NotBlank + @Email / @Pattern
Integer / Long / BigDecimal@NotNull + @Min/@Max または @DecimalMin/@Digits
LocalDate など日時@NotNull + @Past / @FutureOrPresent
Boolean@NotNull + @AssertTrue
List / Map@NotEmpty + @Size(max)、要素は List<@NotBlank String>
ネストDTO@NotNull + @Valid

要注意点は 4 つだけ覚えておけば大丈夫です。null を弾くのは @NotNull 系だけ、@NotBlank は文字列専用、double/float@Min/@Max@Positive 系は使わない、@Email@NotBlank と併用する。この 4 つを押さえたうえで、この表を手元に置いて DTO を書いてみてください。