LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 1319 min read

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 You Will Learn
  • 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>
Note

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.

POST /api/signup with an invalid body
{
"name": "",
"email": "not-an-email",
"age": 15,
"password": "123"
}
400 Bad Request (default response)

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.

@RestControllerAdvice
public 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
}
Behavior

Click Run to see what this code prints.

Common Mistakes

Avoid These 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.

Next Lesson →

Testing Dependencies