Validation and exception handling
Bean Validation on request bodies, the null rules behind @NotNull, @NotEmpty and @NotBlank, and how Spring picks the @ExceptionHandler that turns an exception into a status.
After this lesson you can
- Validate a request body and return every field error in one 400 response
- Choose correctly between @NotNull, @NotEmpty and @NotBlank
- Predict which @ExceptionHandler handles an exception, local or in an advice
Bean Validation (the jakarta.validation API, implemented by Hibernate
Validator) checks constraints declared as annotations. Spring Boot's web
starter does not include it; add spring-boot-starter-validation.
public record NewCustomer(
@NotBlank String name,
@NotNull @Email String email,
@Size(min = 8, max = 64) String password,
@Valid @NotNull Address address) {} // @Valid cascades into the nested object
@PostMapping("/customers")
CustomerDto create(@RequestBody @Valid NewCustomer body) { ... }
When the body violates any constraint, the controller method never
runs: Spring throws MethodArgumentNotValidException, which becomes a
400 Bad Request. The validator does not stop at the first problem —
it reports every violated constraint, so the client can fix all of
them in one round trip. Each violation carries a property path —
name, address.city, or lines[2].sku for an element of a list —
and the constraints of a nested object are only checked when the field
holding it is marked @Valid.
null is (mostly) valid
Almost every constraint treats null as valid. @Size(min = 8) and
@Email say nothing about a missing value; that is the job of the three
"not" constraints, which differ precisely in what they reject:
import java.util.*; public class Solution { static boolean notNull(String v) { return v != null; } static boolean notEmpty(String v) { return v != null && !v.isEmpty(); } static boolean notBlank(String v) { return v != null && !v.isBlank(); } static boolean size3to10(String v) { return v == null || (v.length() >= 3 && v.length() <= 10); } // null is valid public static List<String> run() { List<String> rows = new ArrayList<>(); String[] inputs = {null, "", " ", "ab"}; for (String v : inputs) { String shown = v == null ? "null" : "\"" + v + "\""; rows.add(shown + " NotNull=" + notNull(v) + " NotEmpty=" + notEmpty(v) + " NotBlank=" + notBlank(v) + " Size(3,10)=" + size3to10(v)); } return rows; }}@NotNull rejects only null. @NotEmpty also rejects a zero-length
string (or empty collection). @NotBlank, for strings only, also
rejects whitespace. A required text field is almost always @NotBlank;
a required number or object is @NotNull.
Turning exceptions into responses
An exception that escapes a controller method is offered to
@ExceptionHandler methods. They can live in the controller itself
(handling only that controller's exceptions) or in a
@RestControllerAdvice class that applies across controllers.
@RestControllerAdvice
class ApiErrors {
@ExceptionHandler(OrderNotFoundException.class)
ProblemDetail notFound(OrderNotFoundException e) {
return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
}
}
The resolution rules interviewers probe:
- Handlers declared in the controller that threw are tried first. If one of them matches, it is used — even if an advice has a more specific handler.
- Otherwise the advice classes are tried (in
@Orderorder). - Within one class, the handler for the closest type in the
exception's hierarchy wins: for
OrderNotFoundException extends NotFoundException extends RuntimeException, a handler forNotFoundExceptionbeats one forRuntimeException.
import com.fasterxml.jackson.databind.ObjectMapper;import java.util.*; public class Solution { record FieldError(String field, String message) {} static List<FieldError> validate(Map<String, String> body) { List<FieldError> errors = new ArrayList<>(); String name = body.get("name"), email = body.get("email"), password = body.get("password"); if (name == null || name.isBlank()) errors.add(new FieldError("name", "must not be blank")); if (email == null) errors.add(new FieldError("email", "must not be null")); else if (!email.contains("@")) errors.add(new FieldError("email", "must be a well-formed email address")); if (password != null && (password.length() < 8 || password.length() > 64)) errors.add(new FieldError("password", "size must be between 8 and 64")); return errors; } public static String run() throws Exception { Map<String, String> body = new HashMap<>(); body.put("name", " "); body.put("email", "not-an-email"); body.put("password", "short"); List<FieldError> errors = validate(body); Map<String, Object> problem = new LinkedHashMap<>(); problem.put("type", "about:blank"); problem.put("title", "Bad Request"); problem.put("status", 400); problem.put("detail", errors.size() + " field(s) are invalid"); problem.put("errors", errors); // a record serialises its fields in declaration order return new ObjectMapper().writeValueAsString(problem); }}Common mistakes
- Using
@NotNullon a required name and accepting""or" "— only@NotBlankrejects both, as the first example shows. - Forgetting
@Validon the@RequestBodyparameter (nothing is validated) or on a nested object field (its constraints are skipped). - A catch-all
@ExceptionHandler(Exception.class)that returns 500 for everything, swallowing the 400s and 404s Spring would have produced, and often leaking exception messages to clients.
What interviewers ask
- Difference between
@NotNull,@NotEmptyand@NotBlank? — what each rejects: null; null or empty; null, empty or whitespace only. - A controller has a handler for
RuntimeExceptionand a@ControllerAdvicehas one forOrderNotFoundException. Which runs? — the controller's own handler; local handlers are tried before any advice. - How do you return consistent error responses across an API? — one
@RestControllerAdvice,ProblemDetailbodies, a stable error , and every field error for validation failures.
Exercise · Pro
Which constraints are checked, and where they are reported
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade