Validation & Error Handling

Bean validation

Add spring-boot-starter-validation, annotate the DTO, and mark the parameter @Valid.

public record BookRequest(
        @NotBlank String title,
        @NotBlank @Size(max = 100) String author,
        @Min(1450) @Max(2100) int year,
        @Email String contact) {}
@PostMapping
BookResponse create(@Valid @RequestBody BookRequest req) {  }

A violation throws MethodArgumentNotValidException → 400 before your method runs.

AnnotationChecks
@NotNull / @NotBlank / @NotEmptypresent / non-blank text / non-empty collection
@Size(min, max)length or size
@Min / @Max / @Positivenumbers
@Email / @Patternformat
@Past / @Futuredates

Validate parameters too:

@Validated                                       // on the class
@GetMapping("/search")
List<Book> search(@RequestParam @NotBlank String q) {  }

Your own exceptions

public class BookNotFoundException extends RuntimeException {
    public BookNotFoundException(long id) {
        super("No book with id " + id);
    }
}

One place for error responses

@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(BookNotFoundException.class)
    ProblemDetail notFound(BookNotFoundException ex) {
        var pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
        pd.setTitle("Book not found");
        return pd;
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    ProblemDetail invalid(MethodArgumentNotValidException ex) {
        var pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
        pd.setTitle("Validation failed");
        pd.setProperty("errors", ex.getBindingResult().getFieldErrors().stream()
                .collect(toMap(FieldError::getField, FieldError::getDefaultMessage)));
        return pd;
    }
}

@RestControllerAdvice applies to every controller.

RFC 9457 problem details

ProblemDetail is the standard error format:

{
  "type": "about:blank",
  "title": "Book not found",
  "status": 404,
  "detail": "No book with id 42",
  "instance": "/api/books/42"
}

Turn it on for the framework’s own errors too:

spring.mvc.problemdetails.enabled=true

Shortcut for simple cases

throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No book with id " + id);

Fine in small apps; a typed exception plus advice scales better.

Warning

Never return the stack trace to clients. Log it with the request id and send the client a short message.

Logging the failure

private static final Logger log = LoggerFactory.getLogger(ApiExceptionHandler.class);

@ExceptionHandler(Exception.class)
ProblemDetail unexpected(Exception ex) {
    log.error("Unhandled exception", ex);
    return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Internal error");
}

★ Exercises

  1. Add validation to BookRequest and post an invalid body — read the response.
  2. Write BookNotFoundException and map it to 404 with ProblemDetail.
  3. Return field-level messages for validation errors.
  4. Add a catch-all handler that logs and returns 500 without details.
  5. Which of your endpoints should answer 409 instead of 400?