@Valid Annotation: Validating Request Data in Spring Boot
When developing web applications or REST APIs with Spring Boot, validating request data is an unavoidable task. For example, when registering a user, you need to check whether fields like name and email are not empty and are in the correct format.
This is where the @Valid annotation comes in handy. This article explains the basic role of @Valid through to how to actually define validation rules, in a practical, hands-on way.
What Is @Valid?
@Valid is an annotation that triggers validation processing conforming to the Jakarta Bean Validation (formerly JSR 303/380) specification in Java. In Spring Boot, adding spring-boot-starter-validation lets you easily validate controller arguments and Java objects.
However, @Valid itself only plays a triggering role — it marks “this object should be validated.” What exactly gets validated, and how, is defined by the constraint annotations placed on the fields.
Why Is It So Commonly Used in API Development?
In a Spring Boot REST API, JSON received from the client is mapped to a POJO, and by adding @Valid, Spring automatically performs validation.
@PostMapping("/users")
public ResponseEntity<String> createUser(@RequestBody @Valid UserRequest userRequest) {
return ResponseEntity.ok("User created");
}
If the request is invalid, Spring throws MethodArgumentNotValidException, which lets you design an appropriate error response. Note that the order of @Valid and @RequestBody doesn’t affect the behavior either way — just pick whichever order your team finds more readable and stick with it.
Validation Content Is Defined via Annotations
Once an object is enabled for validation via @Valid, you can finely control what gets validated 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 address format is invalid")
private String email;
}
As shown above, you can also combine multiple constraints on a single field. By specifying the message attribute, you can define custom error messages as well.
Commonly Used Constraint Annotations
@Min and @Max can also be used on primitive types like int and long. Unlike reference-type-only constraints such as @NotNull, you don’t need to worry about boxing, so feel free to use them for range checks on primitive fields like age.
The regexp attribute of @Pattern is a Java string literal, so backslashes need to be double-escaped. This often trips people up when writing a regex like a postal code pattern — forgetting the escape in \d{3}-\d{4} is a common mistake. @Pattern also has a flags attribute; specifying Pattern.Flag.CASE_INSENSITIVE allows case-insensitive matching.
Here’s a quick reference table of 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; |
Hibernate Validator also provides proprietary extensions such as @URL, @Length, and @Range, which are available via spring-boot-starter-validation. Import them from the org.hibernate.validator.constraints package.
Note that in Spring Boot 3.x, the import path is jakarta.validation.constraints.*. This changed from javax.validation.constraints.* in the 2.x series, so be careful.
One point of confusion is how to choose between @NotNull, @NotEmpty, and @NotBlank. @NotNull only rejects null, so an empty string "" passes. @NotEmpty rejects null and zero length, but a whitespace-only string " " passes. For string fields, the standard approach is to use @NotBlank, which rejects null, empty, and whitespace-only values all at once.
Validating Nested Objects
@Valid can also apply validation recursively to nested objects.
public class OrderRequest {
@Valid
private Address address;
}
In this case, the validation rules defined inside the Address class are also applied. Conversely, even if the parent DTO is validated in the controller, the child DTO’s fields won’t be recursively validated unless @Valid is present on them. For nested structures, make sure to check the annotations on both the parent and child.
Manual Validation in the Service Layer
In classes other than controllers (for example, the Service layer), you can explicitly run validation using a 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 logic
}
}
Differences from @Validated and When to Use Each
@Validated is a Spring Framework-specific annotation that provides group specification and the ability to automatically validate method arguments when attached to a class — features that @Valid doesn’t have. Note that simply attaching @Valid to an argument does not trigger method validation. The concepts behind group design and method validation in the Service layer are covered in depth in the follow-up article How to Implement Group-Based Validation and Method Validation with Spring Boot’s @Validated Annotation.
A Minimal Example of Group Validation
Using @Validated, you can switch which fields are required between creation and update — group-based validation.
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 @Valid doesn’t support group specification, use @Validated(Group.class) when you want to change behavior between creation and update.
Comparison Table: @Valid vs. @Validated
The two are similar but differ in where they can be used and what features they offer. Here’s a quick reference table for when you’re unsure in practice.
| Aspect | @Valid (Jakarta) | @Validated (Spring) |
|---|---|---|
| Origin | Jakarta Bean Validation specification | Spring Framework-specific |
| Main use | Controller arguments, recursive validation of nested DTOs | Group validation, method argument validation |
| Group specification | Not supported | Supported (e.g. @Validated(OnCreate.class)) |
| Exception type | MethodArgumentNotValidException | ConstraintViolationException |
| Attachable to a class | No (fields/arguments only) | Yes (enables validation of the entire bean) |
| Nested validation | Recursive application to child DTO fields is straightforward | Typically combined with @Valid for nesting |
In short, the safest practical rule of thumb is: use @Valid for standard validation of a controller’s @RequestBody, and use @Validated for method argument validation in the Service layer or when you need different rules for create vs. update.
Standardizing Error Responses
If you’re using @Valid, it’s important to standardize the return format for MethodArgumentNotValidException up front. If this varies every time, it increases implementation cost on the frontend side.
@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
));
}
}
Fixing the response keys (code, errors, field, message) like this makes things stable for consumers. For the overall design of @RestControllerAdvice including other exceptions, see How to Return Unified Error Responses in a Spring Boot REST API.
With Spring Boot 3, you can also just set spring.mvc.problemdetails.enabled=true in application.properties to return responses in RFC 9457-compliant application/problem+json format. However, this doesn’t include a per-field error list by default, so practical extension methods are covered in How to Standardize Error Responses with Spring Boot 3.x’s Problem Details (RFC 9457).
Common Pitfalls
Forgetting to Add the spring-boot-starter-validation Dependency
Since Spring Boot 2.3, spring-boot-starter-web no longer includes validation-related dependencies. If adding @Valid doesn’t seem to do anything, check this dependency first.
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
The version is managed by the Spring Boot BOM, so you don’t need to specify it individually. For how starters work, see What Is a Spring Boot Starter?, and for the javax → jakarta replacement steps when upgrading from 2.x, see Spring Boot 2.x to 3.x Migration Guide.
Constraints on @RequestParam / @PathVariable Aren’t Validated
Because @Valid targets beans (objects), it has no effect on individual values like @RequestParam or @PathVariable. To validate a single value, attach a constraint annotation directly to the method argument.
@RestController
@Validated
public class UserController {
@GetMapping("/users/{id}")
public UserResponse find(@PathVariable @Min(1) Long id,
@RequestParam(required = false) @Size(max = 20) String keyword) {
// ...
}
}
From Spring Boot 3.2 (Spring Framework 6.1) onward, if constraints are attached to a controller’s method arguments, Spring MVC performs method validation out of the box — even without @Validated on the class — and throws HandlerMethodValidationException on failure. If @Validated is attached to the class, validation still happens via the traditional AOP-based mechanism and throws ConstraintViolationException. Which exception gets thrown directly affects your @RestControllerAdvice handler design, so it’s worth checking which one applies for the version you’re using.
Specifying a Group Disables the Default Group’s Constraints
When you specify a group, such as with @Validated(OnCreate.class), constraints without a group specified (the implicit Default group) are no longer validated. This is usually the cause when “I added @NotBlank but it’s skipped only on the creation API.” If you want existing constraints to run as well, either specify multiple groups or have your group interface extend Default.
import jakarta.validation.groups.Default;
// Extending Default means existing constraints are also validated when OnCreate is specified
public interface OnCreate extends Default {}
@PostMapping("/users")
public ResponseEntity<?> create(@RequestBody @Validated(OnCreate.class) UserRequest req) {
return ResponseEntity.ok().build();
}
// Or specify multiple groups inline
// @Validated({OnCreate.class, Default.class})
If you want to control the validation order, use @GroupSequence. If a violation occurs in an earlier group, subsequent groups aren’t evaluated, which is useful for cases like “run the heavy constraints only after the format checks pass.”
@GroupSequence({Default.class, OnCreate.class})
public interface CreateSequence {}
// @Validated(CreateSequence.class) validates in the order Default → OnCreate
Groups Don’t Propagate to Nested DTOs
A nested field annotated with @Valid is validated under the same group as the parent. If you want the child DTO to be validated under a different group (or Default), use @ConvertGroup to convert the group.
public class OrderRequest {
@Valid
@ConvertGroup(from = OnCreate.class, to = Default.class)
private Address address;
}
Attaching @NotNull to a Primitive Type
Primitive types like int and boolean can never be null, so attaching @NotNull to them has no effect. If the field is omitted from the JSON, a default value like 0 or false gets set, slipping past validation. If you want to reject “unspecified” values, switch to a wrapper type + @NotNull.
// Bad: when omitted, 0 is set, so @NotNull always passes
@NotNull
private int age;
// Good: when omitted, it becomes null, so @NotNull can catch it
@NotNull
@Min(0)
private Integer age;
As a side note, @RequestParam defaults to required = true, so if it’s omitted entirely, a MissingServletRequestParameterException (400) is thrown. A typical scenario for attaching constraints to a single value is when you’ve set required = false but still want to validate the value if it is provided. Also, for types like Optional<String>, you can validate the contents by writing the constraint on the type argument, like Optional<@NotBlank String>.
Validation Passes but Business Requirements Aren’t Met
Even if @NotBlank or @Email pass, that only guarantees “the value has the correct shape.” Rules that require a database lookup, such as “does this email address already exist,” should be checked in the Service layer. On the other hand, correlation checks like “the start date must be before the end date” are better extracted into a custom validation annotation as a ConstraintValidator, so you can reuse it instead of writing it in the Service layer every time.
Over-Hardcoding Validation Messages
Hardcoding message text directly in Java code means you need to rebuild every time the text changes, and it also makes internationalization impossible. If there’s a possibility you’ll need multi-language support in the future, it’s effective to move messages into messages.properties.
@NotBlank(message = "{validation.name.required}")
private String name;
Define the key like validation.name.required=Name is required, and Spring Boot will automatically switch languages based on the locale resolved by LocaleResolver. The setup steps for MessageSource are covered in How to Implement Internationalization (i18n) in Spring Boot.
Handling Form Input and Testing
For HTML forms using something like Thymeleaf rather than a REST API, attach @Valid to the @ModelAttribute argument and receive a BindingResult immediately after it to return errors to the screen without throwing an exception.
@PostMapping("/users")
public String create(@Valid @ModelAttribute("form") UserForm form, BindingResult result) {
if (result.hasErrors()) {
return "users/new"; // re-display the input screen
}
userService.register(form);
return "redirect:/users";
}
BindingResult must be placed immediately after the argument being validated. If you separate them, MethodArgumentNotValidException is thrown instead. In the template, you can display messages per field with something like th:errors="*{name}".
It’s a good idea to test validation behavior with @WebMvcTest + MockMvc. Send invalid JSON and verify the 400 status and response contents. Since @RestControllerAdvice is also scanned, you can test the standardized response format as well.
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void returns400WhenNameIsBlank() throws Exception {
mockMvc.perform(post("/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"\",\"email\":\"[email protected]\"}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value("VALIDATION_ERROR"))
.andExpect(jsonPath("$.errors[0].field").value("name"));
}
}
For the basics of MockMvc, see How to Write Controller Unit Tests Using MockMvc in Spring Boot.
Summary
Using the @Valid annotation lets you implement input validation in Spring Boot concisely and flexibly. The key points are to define validation rules via constraint annotations on the target fields, and to standardize the error response format up front. When you need group specification or method validation, switch to using @Validated as well to build a robust API.
Related Articles
- How to Implement Group-Based Validation and Method Validation with Spring Boot’s @Validated Annotation (follow-up to this article; deep dive into group design and method validation)
- How to Create Custom Validation Annotations in Spring Boot (implementing custom rules with
ConstraintValidator) - How to Return Unified Error Responses in a Spring Boot REST API (the overall design of
@RestControllerAdvice) - How to Standardize Error Responses with Spring Boot 3.x’s Problem Details (RFC 9457) (returning validation errors in a standard format)
- How to Write Controller Unit Tests Using MockMvc in Spring Boot (ensuring 400 responses via tests)
- How to Validate Spring Boot’s @ConfigurationProperties with Bean Validation (applying the same constraints to configuration values)
- How to Implement Internationalization (i18n) in Spring Boot (switching message locales)