Here is the English translation of the article body.

You can now attach @Valid, but do you still find yourself flipping back and forth through the reference every time you wonder “Should this string use @NotEmpty or @NotBlank?”, “Which constraints work on LocalDate?”, or “Where do I put the Japanese messages?”

This article is a focused reference that puts the Bean Validation constraint annotations (validation annotations) available in Spring Boot 3.x into a single table, covering only the choices that are easy to get wrong and how to override messages with ValidationMessages.properties. How @Valid works and how to handle errors are left to the article on using @Valid, so let’s go straight to the table.

The assumptions are Spring Boot 3.x, Jakarta Bean Validation 3.0, and Hibernate Validator 8. Since Spring Boot 3, the package has changed from javax.validation to jakarta.validation, so be careful when copy-pasting from older articles. The dependency is the following single line (for Maven, add the same artifactId as a dependency).

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

Standard Constraint Annotations at a Glance (Cheat Sheet)

The standard constraints in jakarta.validation.constraints are the following 22. The messages are paraphrases of the English defaults.

AnnotationTarget typesOn nullMain attributesDefault message
@NotNullAnyFails-must not be null
@NullAnyPasses (null required)-must be null
@NotEmptyString, Collection, Map, arrayFails-must not be empty
@NotBlankString (CharSequence)Fails-must not be blank
@SizeString, Collection, Map, arrayPassesmin, maxsize must be between {min} and {max}
@MinInteger types, BigDecimal, BigIntegerPassesvaluemust be greater than or equal to {value}
@MaxSame as abovePassesvaluemust be less than or equal to {value}
@DecimalMinAbove + StringPassesvalue, inclusivemust be greater than (or equal to) {value}
@DecimalMaxAbove + StringPassesvalue, inclusivemust be less than (or equal to) {value}
@PositiveInteger types, BigDecimal, BigIntegerPasses-must be greater than 0
@PositiveOrZeroSame as abovePasses-must be greater than or equal to 0
@NegativeSame as abovePasses-must be less than 0
@NegativeOrZeroSame as abovePasses-must be less than or equal to 0
@DigitsNumeric, StringPassesinteger, fractionnumeric value out of bounds ({integer} integer digits, {fraction} fraction digits)
@PastDate/time typesPasses-must be a past date
@PastOrPresentDate/time typesPasses-must be a date in the past or in the present
@FutureDate/time typesPasses-must be a future date
@FutureOrPresentDate/time typesPasses-must be a date in the present or in the future
@EmailStringPassesregexp, flagsmust be a well-formed email address
@PatternStringPassesregexp, flagsmust match “{regexp}“
@AssertTrueboolean, BooleanPasses-must be true
@AssertFalseboolean, BooleanPasses-must be false

“Integer types” means byte/short/int/long and their wrappers. double/float are not supported by the specification for either @Min/@Max or the @Positive family, so use BigDecimal for decimals (the reason is explained in the numeric section).

As you may notice from the table, only three constraints reject null: NotNull, NotEmpty, and NotBlank. Every other constraint treats null as “not subject to validation” and lets it pass. A field with only @Size(max = 20) will let null straight through, so if the field is required, always combine it with a @NotNull-family constraint. This is the single most common pitfall.

The following sections are supplementary notes for each category in the table.

The Difference Between @NotNull, @NotEmpty, and @NotBlank

The most frequently asked question is the difference between these three. A matrix of types and input values makes it instantly clear.

Input value@NotNull@NotEmpty@NotBlank
nullFailsFailsFails
"" (empty string)PassesFailsFails
" " (whitespace only)PassesPassesFails
"abc"PassesPassesPasses
Empty List / Map / arrayPassesFailsException
List with elementsPassesPassesException

@NotBlank is for CharSequence only. If you attach it to a List or Integer, an UnexpectedTypeException (HV000030) is thrown at validation time. “I put @NotBlank on an Integer for a required check and got a 500 error” is a classic problem.

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

The rule of thumb is simple: use @NotBlank for strings entered by users, @NotEmpty for required lists and Maps, and @NotNull for reference types such as numbers, dates/times, and Booleans. That is all you need to remember.

The Difference Between @Size and @Length (Length Constraints)

@Size is a standard constraint that works not only on strings but also on the element count of Collection, Map, and arrays. @Length, on the other hand, is a Hibernate Validator-specific constraint in org.hibernate.validator.constraints and is for strings only. Everything it can do is covered by @Size, so choose @Size by default.

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

Writing a constraint on the type argument, as in List<@NotBlank String>, validates the elements themselves. @Size is strictly a constraint on the “count”, so be careful not to confuse the two.

Numeric Constraints: @Min/@Max, @DecimalMin/@DecimalMax, the @Positive Family, and @Digits

@Min/@Max take a long boundary and support byte/short/int/long (and their wrappers), BigDecimal, and BigInteger. double/float are not supported by the specification because of rounding errors. They happen to work in Hibernate Validator, but if you are dealing with decimals such as monetary amounts, the safe approach is to use BigDecimal with @DecimalMin.

@DecimalMin/@DecimalMax take the boundary as a string, so you can specify decimals such as "0.01", and setting inclusive = false rejects the boundary value itself. If you only care about the sign, use the @Positive family. If you want to restrict the number of digits, use @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以上
) {}

For required numeric checks, use wrapper types rather than primitives. With int quantity, the field is set to 0 even when it is missing from the request, so @NotNull becomes meaningless.

Date/Time Constraints: The @Past/@Future Family and java.time

There are four constraints for dates and times, and the only difference is whether “the present” is included.

  • @Past means before now, and @PastOrPresent means the past including now
  • @Future means after now, and @FutureOrPresent means the future including now

The target types are the full java.time set, including LocalDate/LocalDateTime/LocalTime/Instant/ZonedDateTime/OffsetDateTime/Year/YearMonth/MonthDay, plus java.util.Date/Calendar.

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

These also treat null as passing, so if the field is required, do not forget to add @NotNull.

Email Addresses and Regular Expressions: @Email and @Pattern

@Email became a standard constraint in Bean Validation 2.0. The Hibernate-specific versions of @Email/@NotEmpty/@NotBlank that used to live in org.hibernate.validator.constraints were removed in Hibernate Validator 7, so in Spring Boot 3.x you only need the standard versions in jakarta.validation.constraints. The check is fairly lenient, and values like a@b pass. Importantly, Email treats an empty string as valid. If the field is required, always combine it with @NotBlank.

To enforce a format with a regular expression, use @Pattern. You can specify options such as case-insensitivity via 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
) {}

Once your regular expressions get complicated, it is easier to maintain them by giving them a name that conveys the intent and extracting them into a custom validation annotation.

Boolean Constraints: @AssertTrue/@AssertFalse

These apply only to boolean/Boolean, and the typical use case is a terms-of-service agreement checkbox. When a Boolean is null, @AssertTrue also passes, so if agreement is mandatory, combine it with @NotNull. As a small trick, attaching it to an isXxx() method gives you a simple way to write cross-field checks.

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() is visible to Jackson as a validPeriod property, so if you reuse this record for responses as well, add @JsonIgnore to it.

Hibernate Validator-Specific Constraints

Spring Boot’s default implementation is Hibernate Validator, so its proprietary constraints can be used as normal. Just note that the package is org.hibernate.validator.constraints. None of the ones sharing a name with standard constraints remain, so if you remember that standard constraints are in jakarta.validation.constraints and Hibernate-specific ones are in org.hibernate.validator.constraints, you will never be confused about imports.

AnnotationTarget typesPurpose
@Length(min, max)StringCharacter count limit (can be replaced by @Size)
@Range(min, max)Numeric, StringShorthand for @Min + @Max
@URLStringURL format (protocol/host/port can also be specified)
@UniqueElementsCollectionChecks for duplicate elements
@CreditCardNumberStringCredit card number (Luhn)
@LuhnCheckStringGeneral-purpose Luhn check
@ISBNStringISBN-10/13
@EANStringEAN-8/13
@CodePointLengthStringLength limit that counts surrogate pairs as one character
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
) {}

Using proprietary constraints creates a dependency on Hibernate Validator, but switching to another implementation in Spring Boot is almost unheard of, so in practice you do not need to worry about it.

Cascade with @Valid for Nested Objects and List Elements

It is easy to forget when you only look at the table, but the contents of nested DTOs and List<DTO> are not evaluated unless you add @Valid.

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

For how @Valid behaves and how to handle BindingResult and MethodArgumentNotValidException, see the article on using @Valid and the article on unifying error responses. Validation groups and method validation are covered in the article on @Validated.

Overriding Error Messages with ValidationMessages.properties

The place to replace messages is src/main/resources/ValidationMessages.properties. Just put it at the root of the classpath and Hibernate Validator loads it automatically.

Standard messages are defined under keys of the form {jakarta.validation.constraints.ConstraintName.message}, so writing the same key overrides them all at once. Since @Size applies to more than strings, it is safer not to limit the wording to “characters”. Define your own keys at the same time.

# 標準メッセージの一括上書き
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=ユーザー名は必須です

To change the message for an individual field, write it in the message attribute. You can reference annotation attributes in curly braces, such as {min}/{max}/{value}/{regexp}, and embed the input value itself with ${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} can end up putting personal information such as email addresses or names into logs and responses, so be selective about where you use it.

If you want to localize messages and switch them based on Accept-Language, either add ValidationMessages_en.properties or consolidate into messages_en.properties. See the i18n article for details. Validation of @ConfigurationProperties is covered in the article on validating configuration values.

Settings to Prevent Garbled Japanese Messages

Older articles describe converting text to \u30e6... with native2ascii, but since Java 9, properties files are read as UTF-8 by default, so you can write Japanese directly in ValidationMessages.properties without any problem. If the characters are still garbled, suspect the following three things.

  • Whether the save encoding of your IDE or editor is set to Shift_JIS (MS932)
  • If you write Japanese directly in the message attribute, the compile-time encoding of the Java source
  • Whether you wrote the message in messages.properties or ValidationMessages.properties, and whether the keys match

For the second point, UTF-8 is the default from Java 18 onward, but on Java 17 in a Windows environment it is reassuring to specify it explicitly. For Gradle, set compileJava.options.encoding = 'UTF-8'. For Maven, set project.build.sourceEncoding to UTF-8 (this is already configured if you use the Spring Boot parent POM).

A note on the third point. From Spring Boot 2.6 onward (including 3.x), the auto-configured Validator is integrated with MessageSource out of the box. A {key} such as {user.name.required} or {jakarta.validation.constraints.NotBlank.message} is first resolved against the file specified by spring.messages.basename (messages.properties by default), and if not found, it falls back to ValidationMessages.properties and then to the messages built into Hibernate Validator. In other words, the choice is ValidationMessages.properties if you want to keep Bean Validation messages separate, or messages.properties if you want to centralize them with the rest of your application’s messages. The encoding on the messages.properties side follows spring.messages.encoding (UTF-8 by default), so you can write Japanese directly there as well.

Note that the approach found in older articles, defining your own LocalValidatorFactoryBean and calling setValidationMessageSource, is unnecessary in Spring Boot 3.x. Defining your own Validator Bean backs off Boot’s auto-configuration, removing the fallback mechanism provided by MessageInterpolatorFactory, so ValidationMessages.properties is no longer read. Think of it as an option for when you want to switch ValidationMessages.properties over to MessageSource entirely, and leave it alone unless you have a specific reason.

Summary: Constraint Selection Chart by Field Type

Finally, here is a reverse lookup by type.

Field typeConstraints to start with
String (required)@NotBlank + @Size(max)
String (with a format)@NotBlank + @Email / @Pattern
Integer / Long / BigDecimal@NotNull + @Min/@Max or @DecimalMin/@Digits
LocalDate and other date/time types@NotNull + @Past / @FutureOrPresent
Boolean@NotNull + @AssertTrue
List / Map@NotEmpty + @Size(max), elements as List<@NotBlank String>
Nested DTO@NotNull + @Valid

There are only four caveats to remember. Only the @NotNull family rejects null, @NotBlank is for strings only, do not use @Min/@Max or the @Positive family on double/float, and always pair @Email with @NotBlank. With these four in mind, keep this table at hand and try writing your DTOs.