When developing web applications or REST APIs with Spring Boot, validation of request data is an unavoidable process. For example, when registering a user, you need to check whether the name and email address are not empty and whether the format is correct.
This is where the @Valid annotation comes in handy. In this article, we will provide a practical explanation, from the basic role of @Valid to how to define actual validation rules.
What is @Valid?
@Valid is an annotation that triggers validation processing compliant with the Jakarta Bean Validation (formerly JSR 303/380) specification in Java. In Spring Boot, by introducing spring-boot-starter-validation, you can easily perform validation on controller arguments and Java objects.
However, @Valid itself only plays a trigger-like role of saying “make this object a target of validation.” What is validated and how it is validated are defined by the constraint annotations attached to the fields.
Why is it frequently used in API development?
In Spring Boot REST APIs, it is common to receive request data from clients in formats like JSON. At this time, by mapping the received data to a POJO (Plain Old Java Object) and adding @Valid, Spring will automatically execute validation.
@PostMapping("/users")
public ResponseEntity<String> createUser(@RequestBody @Valid UserRequest userRequest) {
return ResponseEntity.ok("User created");
}
This makes it possible to design a system where, if the request is invalid, Spring throws a MethodArgumentNotValidException and returns an appropriate error response.
Validation content is defined by annotations
For objects where validation processing has been enabled by @Valid, you can finely control the validation content by attaching constraint annotations to each field.
public class UserRequest {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 20, message = "Name must be between 2 and 20 characters")
private String name;
@Email(message = "Email format is invalid")
private String email;
}
In this way, it is also possible to combine multiple constraints for each field.
Commonly Used Constraint Annotations
As a more practical quick reference, here is a table organized with target types and typical code examples.
| Annotation | Target Type | Code Example |
|---|---|---|
@NotNull | Any | @NotNull private Long id; |
@NotBlank | String | @NotBlank private String name; |
@NotEmpty | String / Collection / Array | @NotEmpty private List<String> tags; |
@Size(min, max) | String / Collection / Array | @Size(min=2, max=20) private String name; |
@Email | String | @Email private String email; |
@Pattern(regexp) | String | @Pattern(regexp="\\d{3}-\\d{4}") private String zip; |
@Min / @Max | Numeric | @Min(0) @Max(120) private int age; |
@Positive / @Negative | Numeric | @Positive private BigDecimal price; |
@Past / @Future | Date | @Past private LocalDate birthday; |
As Hibernate Validator-specific extensions, there are also @URL, @Length, @Range, etc., which can be used via spring-boot-starter-validation. Import them from the org.hibernate.validator.constraints package.
Note that in Spring Boot 3.x, the import path has been changed to jakarta.validation.constraints.*. Be careful, as this is changed from javax.validation.constraints.* in the 2.x series.
The constraint annotations available for validation include the following.
| Annotation | Overview |
|---|---|
@NotNull | Must not be null |
@NotBlank | Empty strings or whitespace-only strings are prohibited |
@NotEmpty | Empty collections, arrays, or strings are prohibited |
@Size(min=, max=) | Constraints on length or number of elements |
@Email | Whether it is in email format |
@Pattern(regexp=) | Whether it matches a regular expression |
@Min, @Max | Numeric range constraints |
@Positive, @Negative | Whether the number is positive or negative |
@Past, @Future | Whether the date is in the past or future |
You can also define your own error messages by specifying the message attribute on these annotations.
Validation of Nested Objects
@Valid can recursively apply validation to nested objects as well.
public class OrderRequest {
@Valid
private Address address;
}
In this case, the validation rules defined within the Address class are also applied.
Manual Validation in the Service Layer
In classes other than the controller (for example, the Service layer), you can explicitly execute validation by using the Validator.
@Service
public class UserService {
private final Validator validator;
public UserService(Validator validator) {
this.validator = validator;
}
public void register(UserRequest request) {
Set<ConstraintViolation<UserRequest>> violations = validator.validate(request);
if (!violations.isEmpty()) {
throw new IllegalArgumentException("Validation failed: " + violations);
}
// Registration processing
}
}
Differences from @Validated and How to Use Them
Minimal Example of Group Validation
By using @Validated, you can perform group-specific validation, such as switching required fields between registration and update.
public interface OnCreate {}
public interface OnUpdate {}
public class UserRequest {
@Null(groups = OnCreate.class)
@NotNull(groups = OnUpdate.class)
private Long id;
@NotBlank(groups = {OnCreate.class, OnUpdate.class})
private String name;
}
@PostMapping("/users")
public ResponseEntity<?> create(@RequestBody @Validated(OnCreate.class) UserRequest req) {
return ResponseEntity.ok().build();
}
Since group specification is not possible with @Valid, use @Validated(Group.class) when you want to change behavior between registration and update.
Comparison Table of @Valid and @Validated
The two are similar, but there are differences in where they can be used and their features. Here is a quick reference to help when you are unsure in practice.
| Perspective | @Valid (Jakarta) | @Validated (Spring) |
|---|---|---|
| Origin | Jakarta Bean Validation specification | Spring Framework proprietary |
| Main use | Recursive validation of controller arguments and nested DTOs | Group validation, method argument validation |
| Group specification | Not possible | Possible (@Validated(OnCreate.class), etc.) |
| Exception type | MethodArgumentNotValidException | ConstraintViolationException |
| Applied to class | Not possible (only on fields/arguments) | Possible (enables validation of the entire Bean) |
| Nested validation | Recursive application to child DTO fields is intuitive | For nesting, it is common to use @Valid in combination |
To summarize concisely, the standard validation for @RequestBody on a controller is @Valid, and when you want to validate method arguments in the Service layer or switch rules between registration/update, use @Validated. This division of use causes the fewest issues in practice.
Spring’s proprietary @Validated annotation can also be used for similar purposes as @Valid. The main difference is that it allows for more flexible control such as group validation and method-level validation.
@Component
@Validated
public class UserValidator {
public void validate(@Valid UserRequest request) {
// Automatically validated
}
}
DTO Design Rules in Practice
When you consider operations, DTO validation will be easier to maintain if you align with the following rules.
- Separate API input-only DTOs from DB entities
- Limit the responsibility of each field and avoid excessive validation
- Design messages with awareness of whether they are for users or for logs
- Use
@NotNulland@NotBlankproperly according to purpose (prioritize@NotBlankfor strings)
Rather than “adding constraints to everything just in case,” defining only what is necessary as an input contract makes it more resilient to changes.
Standardizing Error Responses
If you use @Valid, it is important to standardize the return format of MethodArgumentNotValidException from the beginning.
If this varies every time, the implementation cost on the frontend side will increase.
@RestControllerAdvice
public class ApiExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException ex) {
var errors = ex.getBindingResult().getFieldErrors().stream()
.map(error -> Map.of(
"field", error.getField(),
"message", error.getDefaultMessage()
))
.toList();
return ResponseEntity.badRequest().body(Map.of(
"code", "VALIDATION_ERROR",
"errors", errors
));
}
}
By fixing the response keys (code, errors, field, message) in this way, handling on the consumer side becomes stable.
Common Pitfalls
Validation passes but business requirements are not met
While annotation constraints are strong at “format checking,” validation of “business rules” is a separate matter.
For example, since “whether the same email address already exists” requires a DB query, additional checks should be performed in the Service layer.
Forgetting to add @Valid to nested DTOs
Even if you add @Valid only to the parent DTO, recursive validation will not occur if @Valid is not on the child DTO field.
When using nested structures, please check both parent and child annotations.
Hard-coding validation messages too much
If there is a possibility of supporting multiple languages in the future, designing with messages.properties is effective.
@NotBlank(message = "{validation.name.required}")
private String name;
Summary
By using the @Valid annotation, you can implement input validation in Spring Boot concisely and flexibly. The actual validation rules are defined by attaching constraint annotations to the target fields. This enhances the robustness of the API while at the same time providing clear error messages to users.
Beyond just APIs, since validation can be performed using the Validator within any component, you can also use it appropriately depending on the business logic.
Please make use of the @Valid annotation to create more robust applications!
Frequently Asked Questions (FAQ)
Should @Valid or @RequestBody be written first?
The operation is the same regardless of which is written first. Spring MVC does not depend on annotation order. For readability, it is good to standardize on one within the team.
Why doesn’t validation run even when @Valid is added?
The common causes are the following three.
spring-boot-starter-validationis not included in the dependencies (be careful, as it is not automatically added since Spring Boot 2.3).- You forgot to add
@Validto the nested DTO field. - You expect method validation in the Service layer, but
@Validatedis not added to the class (@Validalone does not trigger method validation).
What is the difference between MethodArgumentNotValidException and ConstraintViolationException?
The former is thrown when validation fails with @RequestBody @Valid, and the latter is thrown when it fails on method arguments (path parameters or query parameters) of a Bean with @Validated. In @RestControllerAdvice, by preparing handlers that catch both separately, you can unify the error response format.
I want to internationalize validation messages
Define keys in the format validation.name.required=Name is required in messages.properties / messages_en.properties, etc., and reference them with curly braces like @NotBlank(message = "{validation.name.required}") in the constraint annotation. Spring Boot automatically switches according to the language resolved by LocaleResolver.
How to use @NotNull, @NotBlank, and @NotEmpty properly?
@NotNull: OK as long as it is not null (empty string""passes).@NotEmpty: OK as long as it is not null or empty (length 0) (whitespace" "passes).@NotBlank: Prohibits null, empty, and whitespace-only. For string fields, basically using@NotBlankis safe.