Home/Learn/Spring Boot/Bean Validation with @Valid

Bean Validation with @Valid

Intermediate
Web / REST

JSR-303/380 constraints (@NotNull, @Size, @Email) on request DTOs are enforced automatically when @Valid is placed on the method parameter.

Overview

Spring Boot integrates Jakarta Bean Validation (formerly JSR-303/380) via `spring-boot-starter-validation`, which pulls in Hibernate Validator as the reference implementation. Place constraint annotations (`@NotNull`, `@Size`, `@Email`, `@Pattern`, `@Min`, `@Max`, `@Positive`) directly on your request DTO fields. Add `@Valid` (or `@Validated` for group validation) to the controller method parameter and Spring MVC automatically validates before invoking the method. Violations produce a `MethodArgumentNotValidException`, which you should map to a structured 400 response with a global `@ControllerAdvice`. Custom constraints are defined by creating an annotation plus a `ConstraintValidator<A, T>` implementation.

Constraint Annotations on DTOs

Annotate your request DTO with constraint annotations. Nested objects must be annotated with `@Valid` themselves for cascaded validation. `@NotNull` checks for non-null; `@NotBlank` checks non-null AND non-empty string (whitespace-stripped); `@Size` validates collection and string lengths.

Spring Boot — constraint annotations on request DTO
public record CreateUserRequest(
    @NotBlank(message = "Name is required")
    @Size(max = 100, message = "Name must be at most 100 characters")
    String name,

    @NotNull @Email(message = "Must be a valid email address")
    String email,

    @NotNull @Min(18) @Max(120)
    Integer age,

    @Valid                          // cascade validation into the nested object
    @NotNull
    AddressRequest address
) {}

public record AddressRequest(
    @NotBlank String street,
    @Pattern(regexp = "^[0-9]{5}$", message = "ZIP must be 5 digits")
    String zip
) {}

@Valid in Controllers and Handling Errors

Placing `@Valid` on the `@RequestBody` parameter triggers validation before the method body executes. A `MethodArgumentNotValidException` is thrown on failure. Handle it in a `@ControllerAdvice` to return a structured error response — never let the default Spring error page leak to clients.

Spring Boot — @Valid on @RequestBody + @ControllerAdvice
@RestController
@RequestMapping("/users")
class UserController {

    @PostMapping
    ResponseEntity<UserResponse> create(@Valid @RequestBody CreateUserRequest req) {
        // Only reached if validation passes
        return ResponseEntity.status(201).body(userService.create(req));
    }
}

// Global error handler
@RestControllerAdvice
class ValidationExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    Map<String, Object> handleValidation(MethodArgumentNotValidException ex) {
        List<String> errors = ex.getBindingResult().getFieldErrors().stream()
                .map(e -> e.getField() + ": " + e.getDefaultMessage())
                .toList();
        return Map.of("status", 400, "errors", errors);
    }
}

Custom Constraints and @Validated Groups

For business-rule validations (e.g., unique email, cross-field checks), create a custom `@Constraint` annotation + `ConstraintValidator`. `@Validated` with groups selects which subset of constraints to apply — useful when the same DTO is used for both Create (requires ID-less fields) and Update (requires ID).

Spring Boot — custom ConstraintValidator and @Validated groups
// Custom constraint annotation
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueEmailValidator.class)
public @interface UniqueEmail {
    String message() default "Email already registered";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// Validator logic
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
    @Autowired UserRepository repo;

    @Override
    public boolean isValid(String email, ConstraintValidatorContext ctx) {
        return email == null || !repo.existsByEmail(email);
    }
}

// Validation groups
interface OnCreate {}
interface OnUpdate {}

public record UserRequest(
    @NotNull(groups = OnUpdate.class) Long id,
    @NotBlank @UniqueEmail(groups = OnCreate.class) String email
) {}

// Controller uses @Validated(OnCreate.class) instead of @Valid
@PostMapping
ResponseEntity<?> create(@Validated(OnCreate.class) @RequestBody UserRequest req) { ... }

Key Points to Remember

  • 1Add spring-boot-starter-validation to get Hibernate Validator on the classpath
  • 2@Valid on @RequestBody triggers validation; @Validated adds group support
  • 3@NotBlank is stricter than @NotNull — it also rejects empty/whitespace strings
  • 4Nested objects require their own @Valid annotation for cascaded validation
  • 5Handle MethodArgumentNotValidException in @ControllerAdvice for structured 400 responses
  • 6Custom constraints: create a @Constraint annotation + ConstraintValidator<A, T> class

Interview Questions

Sign in to ask Aria
1

What is the difference between @Valid and @Validated in Spring?

MediumCapgemini
2

What is the difference between @NotNull, @NotEmpty, and @NotBlank?

EasyInfosys
3

How would you return a structured JSON error response when validation fails?

MediumAccenture
4

How do you create a custom constraint annotation in Spring Boot?

HardThoughtWorks
5

How do validation groups help when the same DTO is used for Create and Update?

MediumAmazon

Ask Aria about Bean Validation with @Valid

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…