Built-in Constraints
The Jakarta constraints you will use daily, how each treats null, and how to write messages clients can act on.
@NotBlank @Size(max = 80) String name
@NotNull @Positive Integer quantity
@DecimalMin("0.01") @Digits(integer = 10, fraction = 2) BigDecimal price
@Pattern(regexp = "[A-Z]{3}-\\d{4}") String skuThe jakarta.validation.constraints package covers most request rules. Knowing exactly what each one accepts saves you from rules that look strict and are not.
The everyday set
| Constraint | Use it for |
|---|---|
@NotNull | Any type: the value must be present. |
@NotEmpty | Strings, collections, maps, arrays: not null and size above zero. |
@NotBlank | Strings: not null and at least one non-whitespace character. |
@Size(min, max) | Length of a string or size of a collection. |
@Min, @Max, @Positive, @PositiveOrZero | Whole-number ranges. |
@DecimalMin, @DecimalMax, @Digits | Money and other BigDecimal values. |
@Email, @Pattern(regexp) | Formats. |
@Past, @Future, @PastOrPresent, @FutureOrPresent | Dates and times. |
@AssertTrue | A boolean, often a derived one that checks two fields together. |
Null passes almost everything
Apart from the @Not... family, constraints treat null as valid. @Email String email accepts a request with no email at all. That is deliberate, so optional fields can still have a format, but it means a required field needs @NotNull or @NotBlank next to its format rule.
Prefer @NotBlank over @NotEmpty for text: a name of three spaces passes @NotEmpty. For numbers, use a wrapper type (Integer) with @NotNull when the client must send it; a primitive int silently becomes 0 when the field is missing.
Limits that protect you
Put a @Size(max = ...) on every free-text field and every list. It matches the column length, so the database never truncates or rejects the value, and it stops one client from sending a megabyte in a name. @Email is lenient; it will accept user@localhost. Treat it as a typo check and confirm addresses by sending mail.
Messages
Each constraint has a message attribute. The default messages are generic ("must not be blank"). They are interpolated in the request's locale, taken from Accept-Language, so a client that sends de gets German text from Hibernate Validator's bundled translations, while one that sends nothing gets the server's default locale. Set your own text, and use placeholders such as {min} and {max} so the numbers stay in one place:
@Size(min = 8, max = 64, message = "must be between {min} and {max} characters")
String passwordMessages can also be keys in braces, resolved from ValidationMessages.properties or, with Spring Boot, from the application's messages.properties. Keys keep wording out of the code and allow translations.
Example
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.FutureOrPresent;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Set;
public record CreateListingRequest(
(max = 120) String title,
(max = 4000) String description,
(value = "0.01", message = "must be at least {value}")
(integer = 8, fraction = 2)
BigDecimal price,
(regexp = "[A-Z]{3}", message = "must be a three-letter currency code")
String currency,
(1) (999) Integer stock,
(max = 254) String contactEmail,
LocalDate availableFrom,
(max = 10) Set< (max = 30) String> tags) {
}
When to use it
- Cap the length of every free-text field to match its database column.
- Validate a price as a positive BigDecimal with at most two decimal places.
- Accept an optional phone number but reject one that is not in international format.
More examples
Message keys in messages.properties
Reference a key with message = "{listing.title.required}". Spring Boot's validator resolves it from the application's message source, and attribute placeholders such as {value} are filled in after lookup. Overriding jakarta.validation.constraints.NotBlank.message changes the default wording for every @NotBlank without touching each annotation.
# src/main/resources/messages.properties
listing.title.required=Give the listing a title.
listing.price.min=The price must be at least {value}.
# Overrides the default text of a built-in constraint everywhere.
jakarta.validation.constraints.NotBlank.message=This field is required.
Checking two fields together with @AssertTrue
Bean Validation treats an is-prefixed boolean method as a property, so @AssertTrue on isOrdered() runs with the rest of the constraints and reports under the name ordered. It is the quickest way to express a rule across two fields. Once the same rule appears on several DTOs, a class-level custom constraint is cleaner.
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
public record DateRangeQuery(@NotNull LocalDate from, @NotNull LocalDate to) {
@AssertTrue(message = "from must not be after to")
public boolean isOrdered() {
// Leave null handling to @NotNull so the client gets one clear error.
return from == null || to == null || !from.isAfter(to);
}
}
Format rules on optional fields
@Pattern must match the whole value, so no ^ and $ anchors are needed. Because null passes, these fields stay optional while still rejecting nonsense. An empty string is not null, though: "" fails the phone pattern and @Size(min = 1) on website, which is usually what you want for a field the client chose to send.
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
public record UpdateProfileRequest(
@NotBlank @Size(max = 80) String displayName,
// Optional: null is accepted, but if present it must look like a phone number.
@Pattern(regexp = "\\+[1-9]\\d{6,14}", message = "must be in international format, e.g. +61412345678")
String phone,
// Optional, but an empty string is not a website.
@Size(min = 1, max = 200)
@Pattern(regexp = "https://.+", message = "must start with https://")
String website) {
}
Discussion