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.
| Annotation | Target types | On null | Main attributes | Default message |
|---|---|---|---|---|
@NotNull | Any | Fails | - | must not be null |
@Null | Any | Passes (null required) | - | must be null |
@NotEmpty | String, Collection, Map, array | Fails | - | must not be empty |
@NotBlank | String (CharSequence) | Fails | - | must not be blank |
@Size | String, Collection, Map, array | Passes | min, max | size must be between {min} and {max} |
@Min | Integer types, BigDecimal, BigInteger | Passes | value | must be greater than or equal to {value} |
@Max | Same as above | Passes | value | must be less than or equal to {value} |
@DecimalMin | Above + String | Passes | value, inclusive | must be greater than (or equal to) {value} |
@DecimalMax | Above + String | Passes | value, inclusive | must be less than (or equal to) {value} |
@Positive | Integer types, BigDecimal, BigInteger | Passes | - | must be greater than 0 |
@PositiveOrZero | Same as above | Passes | - | must be greater than or equal to 0 |
@Negative | Same as above | Passes | - | must be less than 0 |
@NegativeOrZero | Same as above | Passes | - | must be less than or equal to 0 |
@Digits | Numeric, String | Passes | integer, fraction | numeric value out of bounds ({integer} integer digits, {fraction} fraction digits) |
@Past | Date/time types | Passes | - | must be a past date |
@PastOrPresent | Date/time types | Passes | - | must be a date in the past or in the present |
@Future | Date/time types | Passes | - | must be a future date |
@FutureOrPresent | Date/time types | Passes | - | must be a date in the present or in the future |
@Email | String | Passes | regexp, flags | must be a well-formed email address |
@Pattern | String | Passes | regexp, flags | must match “{regexp}“ |
@AssertTrue | boolean, Boolean | Passes | - | must be true |
@AssertFalse | boolean, Boolean | Passes | - | 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 |
|---|---|---|---|
null | Fails | Fails | Fails |
"" (empty string) | Passes | Fails | Fails |
" " (whitespace only) | Passes | Passes | Fails |
"abc" | Passes | Passes | Passes |
Empty List / Map / array | Passes | Fails | Exception |
List with elements | Passes | Passes | Exception |
@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.
@Pastmeans before now, and@PastOrPresentmeans the past including now@Futuremeans after now, and@FutureOrPresentmeans 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.
| Annotation | Target types | Purpose |
|---|---|---|
@Length(min, max) | String | Character count limit (can be replaced by @Size) |
@Range(min, max) | Numeric, String | Shorthand for @Min + @Max |
@URL | String | URL format (protocol/host/port can also be specified) |
@UniqueElements | Collection | Checks for duplicate elements |
@CreditCardNumber | String | Credit card number (Luhn) |
@LuhnCheck | String | General-purpose Luhn check |
@ISBN | String | ISBN-10/13 |
@EAN | String | EAN-8/13 |
@CodePointLength | String | Length 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
messageattribute, the compile-time encoding of the Java source - Whether you wrote the message in
messages.propertiesorValidationMessages.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 type | Constraints 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.