Validation Dependencies
Learn spring-boot-starter-validation: constraint annotations on a DTO, a custom @Constraint, and how failed validation turns into a 400 response automatically.
Introduction
A controller should never trust the data it receives. Instead of writing manual if-checks for every field on every DTO, Spring Boot lets you declare the rules directly on the class and have them enforced automatically. This lesson covers the dependency that makes that possible.
- What spring-boot-starter-validation adds to your classpath.
- How to annotate a request DTO with constraints like @NotBlank, @Email, and @Min.
- How @Valid triggers validation on an incoming request body.
- How a failed validation turns into a structured 400 response.
- How to write your own custom @Constraint annotation.
spring-boot-starter-validation
This starter pulls in Hibernate Validator, the reference implementation of Jakarta Bean Validation. It is what supplies the actual annotations — @NotBlank, @Email, @Min, @Size, and more — along with the engine that evaluates them.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId></dependency>spring-boot-starter-web does not include this starter automatically as of Spring Boot 2.3+. If @Valid annotations are silently doing nothing, this is almost always why — the dependency needs to be added explicitly.
Validating a Request DTO
Constraints go directly on the fields of your request DTO, and @Valid on the controller parameter tells Spring to evaluate them before the method body runs.
public class SignupRequest {
@NotBlank(message = "Name is required") private String name;
@Email(message = "A valid email is required") @NotBlank private String email;
@Min(value = 18, message = "Must be at least 18 years old") private int age;
@Size(min = 8, message = "Password must be at least 8 characters") private String password;
// getters and setters}
@RestController@RequestMapping("/api/signup")public class SignupController {
@PostMapping public ResponseEntity<String> signup(@Valid @RequestBody SignupRequest request) { // this line only runs if every constraint above passed return ResponseEntity.ok("Account created for " + request.getEmail()); }}How Errors Become a 400
When @Valid fails, Spring throws a MethodArgumentNotValidException before your controller code ever executes, and Spring Boot's default error handler converts it into a 400 Bad Request automatically — no extra code required.
{ "name": "", "email": "not-an-email", "age": 15, "password": "123"}Click Run to see what this code prints.
For full control over the response shape, catch MethodArgumentNotValidException in an @ControllerAdvice / @ExceptionHandler and build your own error body instead of relying on the default one.
@RestControllerAdvicepublic class ValidationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, String>> handleValidation(MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getFieldErrors() .forEach(error -> errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors); }}Building a Custom Constraint
When the built-in annotations are not specific enough — say, enforcing that a username contains no spaces and only lowercase letters — you can write your own @Constraint annotation backed by a validator class.
@Target({ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)@Constraint(validatedBy = UsernameValidator.class)public @interface ValidUsername { String message() default "Username must be lowercase letters and digits only"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {};}
public class UsernameValidator implements ConstraintValidator<ValidUsername, String> {
private static final Pattern PATTERN = Pattern.compile("^[a-z0-9]+$");
@Override public boolean isValid(String value, ConstraintValidatorContext context) { return value != null && PATTERN.matcher(value).matches(); }}
public class SignupRequest {
@ValidUsername private String username;
// other fields}Click Run to see what this code prints.
Common Mistakes
- Forgetting spring-boot-starter-validation as a separate dependency and wondering why @Valid does nothing.
- Adding constraint annotations but forgetting @Valid on the controller parameter — without it, nothing is checked.
- Putting business rules (like "email must not already exist") into a bean validation constraint instead of a service-layer check — bean validation is for shape and format, not database lookups.
- Validating a nested object without @Valid on the nested field — annotations on a nested DTO are silently skipped otherwise.
Frequently Asked Questions
Yes, when combined with @Validated at the class level on the controller, individual @RequestParam or @PathVariable values can carry constraints like @Min directly.
Yes — put @Valid on the nested field itself (for example, @Valid private Address address;) so its constraints are checked too, not just the outer object's.
@NotNull only rejects null. @NotEmpty also rejects an empty string or collection. @NotBlank additionally rejects a string that is only whitespace.
Summary
spring-boot-starter-validation lets you declare data rules directly on your DTOs and have Spring enforce them before your business logic runs, failing fast with a 400 response. Next, we look at how to actually test all of this code with confidence.