Introduction
Javax (Jakarta) Bean Validation does not automatically validate nested objects. If you have an Order with an Address field, adding @NotNull to Address fields does nothing unless the Address field itself is annotated with @Valid. This is the single most common reason nested validation "does not work" — the @Valid annotation is missing on the parent field.
The Problem
1public class Order {
2 @NotNull
3 private String orderId;
4
5 @NotNull
6 private Address shippingAddress; // @NotNull checks if address is null
7 // but does NOT validate Address fields
8}
9
10public class Address {
11 @NotBlank
12 private String street;
13
14 @NotBlank
15 private String city;
16
17 @Size(min = 5, max = 5)
18 private String zipCode;
19}
If you submit an Order with a non-null Address that has a blank street, validation passes. The @NotBlank on street is never checked because the validator does not descend into Address.
The Fix: Add @Valid
1public class Order {
2 @NotNull
3 private String orderId;
4
5 @Valid // <-- This triggers validation of Address fields
6 @NotNull
7 private Address shippingAddress;
8}
@Valid tells the validator to recursively validate the annotated object. Now @NotBlank on street and city, and @Size on zipCode are enforced.
@Valid on Collections
@Valid also works on collections to validate each element:
1public class Order {
2 @NotNull
3 private String orderId;
4
5 @Valid
6 @NotNull
7 private Address shippingAddress;
8
9 @Valid // Validates each LineItem
10 @NotEmpty // At least one item required
11 private List<LineItem> items;
12}
13
14public class LineItem {
15 @NotBlank
16 private String productId;
17
18 @Min(1)
19 private int quantity;
20
21 @DecimalMin("0.01")
22 private BigDecimal price;
23}
Without @Valid on the items list, individual LineItem constraints are ignored even if the list contains invalid items.
Spring MVC: @Valid on Controller Parameters
In Spring MVC, you must also use @Valid on the controller method parameter:
1@RestController
2@RequestMapping("/orders")
3public class OrderController {
4
5 @PostMapping
6 public ResponseEntity<Order> createOrder(
7 @Valid @RequestBody Order order, // @Valid triggers validation
8 BindingResult result) {
9
10 if (result.hasErrors()) {
11 // Return validation errors
12 List<String> errors = result.getFieldErrors().stream()
13 .map(e -> e.getField() + ": " + e.getDefaultMessage())
14 .collect(Collectors.toList());
15 return ResponseEntity.badRequest().body(null);
16 }
17
18 return ResponseEntity.ok(orderService.create(order));
19 }
20}
If you omit @Valid from the @RequestBody parameter, no validation runs at all — neither top-level nor nested.
@Validated vs @Valid
Spring provides @Validated as an alternative to @Valid with support for validation groups:
1// Validation groups
2public interface OnCreate {}
3public interface OnUpdate {}
4
5public class User {
6 @Null(groups = OnCreate.class) // Must be null when creating
7 @NotNull(groups = OnUpdate.class) // Must exist when updating
8 private Long id;
9
10 @NotBlank(groups = {OnCreate.class, OnUpdate.class})
11 private String name;
12
13 @Valid
14 private Address address;
15}
16
17@RestController
18public class UserController {
19
20 @PostMapping("/users")
21 public User create(
22 @Validated(OnCreate.class) @RequestBody User user) {
23 // Only OnCreate constraints are checked
24 return userService.create(user);
25 }
26
27 @PutMapping("/users/{id}")
28 public User update(
29 @Validated(OnUpdate.class) @RequestBody User user) {
30 // Only OnUpdate constraints are checked
31 return userService.update(user);
32 }
33}
@Valid does not support groups — use @Validated when you need group-based validation.
Deeply Nested Objects
@Valid cascades recursively. If Address contains a Country object, add @Valid at each level:
1public class Order {
2 @Valid @NotNull
3 private Address shippingAddress;
4}
5
6public class Address {
7 @NotBlank
8 private String street;
9
10 @Valid @NotNull // Cascades into Country
11 private Country country;
12}
13
14public class Country {
15 @NotBlank
16 private String code;
17
18 @NotBlank
19 private String name;
20}
If you forget @Valid on country, the Country constraints are skipped.
Programmatic Validation
You can also validate objects manually using Validator:
1import javax.validation.Validator;
2import javax.validation.ValidatorFactory;
3import javax.validation.Validation;
4import javax.validation.ConstraintViolation;
5
6ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
7Validator validator = factory.getValidator();
8
9Order order = new Order();
10order.setOrderId("ORD-123");
11order.setShippingAddress(new Address()); // Empty address
12
13Set<ConstraintViolation<Order>> violations = validator.validate(order);
14
15for (ConstraintViolation<Order> v : violations) {
16 System.out.println(v.getPropertyPath() + ": " + v.getMessage());
17}
18// Output:
19// shippingAddress.street: must not be blank
20// shippingAddress.city: must not be blank
21// shippingAddress.zipCode: size must be between 5 and 5
This only works if @Valid is on the shippingAddress field.
Dependency Requirements
Ensure you have the validation implementation on the classpath:
1<!-- Spring Boot (includes Hibernate Validator) -->
2<dependency>
3 <groupId>org.springframework.boot</groupId>
4 <artifactId>spring-boot-starter-validation</artifactId>
5</dependency>
6
7<!-- Or standalone Hibernate Validator -->
8<dependency>
9 <groupId>org.hibernate.validator</groupId>
10 <artifactId>hibernate-validator</artifactId>
11 <version>8.0.1.Final</version>
12</dependency>
In Spring Boot 2.3+, spring-boot-starter-web no longer includes validation automatically. You must add spring-boot-starter-validation explicitly.
Jakarta vs Javax Namespace
Starting with Jakarta EE 9, the package changed from javax.validation to jakarta.validation:
1// Jakarta EE 9+ (Spring Boot 3.x)
2import jakarta.validation.Valid;
3import jakarta.validation.constraints.NotNull;
4
5// Javax (Spring Boot 2.x, Java EE)
6import javax.validation.Valid;
7import javax.validation.constraints.NotNull;
If your annotations come from the wrong namespace, the validator ignores them entirely.
Common Pitfalls
Missing @Valid on the nested field: The number one cause. Without @Valid, the validator skips nested object constraints entirely.
Missing @Valid on the controller parameter: In Spring MVC, the @RequestBody parameter must be annotated with @Valid or @Validated to trigger validation.
Missing spring-boot-starter-validation: Since Spring Boot 2.3, validation is not included in spring-boot-starter-web. Add the validation starter explicitly.
Wrong namespace (javax vs jakarta): Mixing javax.validation annotations with a Jakarta validator (or vice versa) causes silent validation failure.
@Valid on a null field: @Valid on a null field does not trigger validation errors for the nested object's fields — it simply skips validation. Combine with @NotNull to ensure the object exists.
Summary
Add @Valid on nested object fields to enable cascading validation
@Valid works on single objects, collections, and maps
In Spring MVC, annotate @RequestBody parameters with @Valid or @Validated
Use @Validated (Spring-specific) for validation groups
Add spring-boot-starter-validation in Spring Boot 2.3+
Check the namespace: javax.validation for Java EE / Spring Boot 2.x, jakarta.validation for Jakarta EE / Spring Boot 3.x