When developing web applications or REST APIs with Spring Boot, validating request data is a step you cannot skip. For example, at user registration you need to check that the name and email address are not empty and that they are in the correct format.
This is where the @Valid annotation comes in. In this article, we will walk through @Valid in a practical way, from its basic role to how to define actual validation rules.
What Is @Valid?
@Valid is an annotation that triggers validation compliant with the Jakarta Bean Validation specification (formerly JSR 303/380) in Java. In Spring Boot, adding spring-boot-starter-validation lets you easily validate controller arguments and Java objects.
Note, however, that @Valid itself only plays a trigger role, marking “this object is a validation target.” What is validated and how is defined by the constraint annotations placed on the fields.
Why Is It So Widely Used in API Development?
In a Spring Boot REST API, the JSON received from the client is mapped to a POJO, and by adding @Valid, Spring automatically runs validation for you.
@PostMapping("/users")
public ResponseEntity<String> createUser(@RequestBody @Valid UserRequest userRequest) {
return ResponseEntity.ok("User created");
}
When the request is invalid, Spring throws a MethodArgumentNotValidException, which lets you design the API to return an appropriate error response. The order in which you write @Valid and @RequestBody makes no difference to the behavior, so just standardize on whichever your team finds more readable.
Validation Rules Are Defined with Annotations
For an object where validation has been enabled via @Valid, you can control the validation in detail by attaching constraint annotations to each field.
public class UserRequest {
@NotBlank(message = "名前は必須です")
@Size(min = 2, max = 20, message = "名前は2〜20文字で入力してください")
private String name;
@Email(message = "メールアドレスの形式が正しくありません")
private String email;
}
As shown here, you can combine multiple constraints on a single field. By specifying the message attribute, you can also define your own error messages.
Commonly Used Constraint Annotations
Here is a quick reference of the 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 its own extensions such as @URL, @Length, and @Range, which are available through 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 line, so be careful.
A common point of confusion is choosing between @NotNull, @NotEmpty, and @NotBlank. @NotNull rejects only null, so an empty string "" passes. @NotEmpty rejects null and zero-length values, but a whitespace-only " " passes. For string fields, the rule of thumb is to use @NotBlank, which rejects null, empty, and whitespace-only values alike.
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 applied as well. Conversely, even if the parent DTO is validated in the controller, the child DTO will not be validated recursively unless its field carries @Valid. With nested structures, check the annotations on both the parent and the child.
Manual Validation in the Service Layer
In classes other than controllers (for example, the Service layer), you can run validation explicitly by using 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);
}
// 登録処理
}
}
Differences from @Validated and When to Use Each
@Validated is a Spring Framework-specific annotation that provides group specification and the ability to annotate a class so that method arguments are validated automatically, neither of which @Valid offers. Keep in mind that simply putting @Valid on an argument does not trigger method validation. The approach to designing groups and method validation in the Service layer is covered in depth in the follow-up article, How to Implement Group Validation and Method Validation with Spring Boot’s @Validated Annotation.
A Minimal Example of Group Validation
With @Validated, you can perform group-specific validation, such as switching which fields are required between creation 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();
}
Because @Valid cannot specify groups, use @Validated(Group.class) when you want different behavior for create versus update.
Comparison Table: @Valid vs. @Validated
The two look similar, but they differ in where they can be used and what they can do. Here is a quick reference for when you are 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 |
| Applicable to classes | No (fields/arguments only) | Yes (enables validation for the whole Bean) |
| Nested validation | Recursive application to child DTO fields is intuitive | Commonly combined with @Valid for nesting |
In short, the division of labor that causes the fewest problems in practice 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 to switch rules between create and update.
Standardizing the Error Response
If you use @Valid, it is important to standardize the response format for MethodArgumentNotValidException from the start. If this varies from endpoint to endpoint, the implementation cost on the frontend side grows.
@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 this way (code, errors, field, message) makes handling on the consumer side stable. 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, there is also the option of setting spring.mvc.problemdetails.enabled=true in application.properties to return responses in the RFC 9457-compliant application/problem+json format. However, the default does not include a per-field error list, so the practical way to extend it is covered in How to Standardize Error Responses with Problem Details (RFC 9457) in Spring Boot 3.x.
Common Pitfalls
Forgetting to Add the spring-boot-starter-validation Dependency
Since Spring Boot 2.3, spring-boot-starter-web no longer includes the validation-related dependencies. If nothing happens when you add @Valid, check the dependencies 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 there is no need to specify it individually. For how Starters work, see What Is a Spring Boot Starter?, and for the steps to replace javax with jakarta when upgrading from 2.x, see Migration Guide from Spring Boot 2.x to 3.x.
Constraints on @RequestParam / @PathVariable Are Not Validated
Because @Valid targets Beans (objects), it has no effect on single values such as @RequestParam or @PathVariable. To validate single values, attach the constraint annotations directly to the method arguments.
@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 a controller’s method arguments carry constraints, Spring MVC performs built-in method validation even without @Validated on the class, and throws HandlerMethodValidationException on failure. If you do put @Validated on the class, validation is AOP-based as before and results in a ConstraintViolationException. Which exception is thrown directly affects the handler design of your @RestControllerAdvice, so it is safest to verify this once on the version you are using.
Specifying a Group Stops the Default Group Constraints from Running
When you specify a group such as @Validated(OnCreate.class), constraints without a group (the implicit Default group) are no longer validated. When you find that “the @NotBlank is there, but only the create API lets it through,” this is usually the cause. If you want the existing constraints to run as well, either specify multiple groups or have the group interface extend Default.
import jakarta.validation.groups.Default;
// Default を継承しておくと OnCreate 指定時に既存の制約も一緒に検証される
public interface OnCreate extends Default {}
@PostMapping("/users")
public ResponseEntity<?> create(@RequestBody @Validated(OnCreate.class) UserRequest req) {
return ResponseEntity.ok().build();
}
// もしくはその場で複数指定する
// @Validated({OnCreate.class, Default.class})
If you want to control the validation order, use @GroupSequence. If there is a violation in an earlier group, subsequent groups are not evaluated, which makes it well suited to cases like “run the heavy constraints only after the format checks pass.”
@GroupSequence({Default.class, OnCreate.class})
public interface CreateSequence {}
// @Validated(CreateSequence.class) で Default → OnCreate の順に検証
Groups Do Not Propagate to Nested DTOs
A nested field annotated with @Valid is validated with the same group as its parent. If you want to validate the child DTO with a different group (or Default), convert the group with @ConvertGroup.
public class OrderRequest {
@Valid
@ConvertGroup(from = OnCreate.class, to = Default.class)
private Address address;
}
Putting @NotNull on a Primitive Type
Primitive types such as int and boolean can never be null, so @NotNull on them is meaningless. If the field is omitted from the JSON, the default value 0 or false is set and the check is bypassed. If you want to reject “not specified,” switch to a wrapper type + @NotNull.
// NG: 省略されると 0 が入り、@NotNull は常に通る
@NotNull
private int age;
// OK: 省略されると null になり、@NotNull で弾ける
@NotNull
@Min(0)
private Integer age;
As a side note, @RequestParam defaults to required = true, so a missing parameter results in a MissingServletRequestParameterException (400). The typical scenario for attaching constraints to a single value is when you set required = false and then want to “validate the value’s constraints only if it was provided.” Also, for types like Optional<String>, you can validate the contents by writing the constraint on the type argument, as in Optional<@NotBlank String>.
Validation Passes but Business Requirements Are Not Met
Even when @NotBlank or @Email passes, that only guarantees that “the value has the correct shape.” Rules that require a database lookup, such as “does the same email address already exist,” are checked in the Service layer. On the other hand, cross-field checks like “the start date must be before the end date” are more reusable when extracted into a ConstraintValidator as a custom validation annotation rather than written in the Service layer every time.
Hardcoding Too Many Validation Messages
If you write message text directly in Java code, every wording change requires a rebuild, and you cannot support multiple languages. If there is any chance you will need multilingual support in the future, moving messages into messages.properties is an effective design.
@NotBlank(message = "{validation.name.required}")
private String name;
Define keys such as validation.name.required=名前は必須です, and Spring Boot will switch them automatically according to the language resolved by LocaleResolver. The steps for configuring MessageSource are summarized in How to Implement Internationalization (i18n) in Spring Boot.
Handling Form Input and Tests
For HTML forms such as Thymeleaf rather than a REST API, put @Valid on the @ModelAttribute argument and receive a BindingResult immediately after it. This lets you 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"; // 入力画面を再表示
}
userService.register(form);
return "redirect:/users";
}
The BindingResult must be placed immediately after the argument being validated. If they are separated, a MethodArgumentNotValidException is thrown. On the template side, you can display messages per field with something like th:errors="*{name}".
It is reassuring to test the validation behavior with @WebMvcTest + MockMvc. Send invalid JSON and verify the 400 status and the response body. Since @RestControllerAdvice is also included in the scan, you can test the standardized response format as well.
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@Test
void 名前が空なら400を返す() 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 with MockMvc in Spring Boot.
Summary
The @Valid annotation lets you implement input validation in Spring Boot concisely and flexibly. The key points are to define validation rules with constraint annotations on the target fields and to standardize the error response format from the start. When you need group specification or method validation, switch to @Validated as appropriate and build robust APIs.
Related Articles
- How to Implement Group Validation and Method Validation with Spring Boot’s @Validated Annotation (the follow-up to this article, going deeper into group design and method validation)
- How to Create Custom Validation Annotations in Spring Boot (implementing your own 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 Problem Details (RFC 9457) in Spring Boot 3.x (returning validation errors in a standard format)
- How to Write Controller Unit Tests with MockMvc in Spring Boot (guaranteeing 400 responses through 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)