Card 04/ 06

WalkthroughDifficulty: Advanced2 min

Turning Validation Errors Into a Field-by-Field Response

Catch the exception @Valid throws and tell the client exactly which fields to fix.

When @Valid rejects a request, Spring throws MethodArgumentNotValidException, which carries every field that broke a rule and the message for each. Handling that exception yourself turns a bare 400 into a response that lists each field and what is wrong with it.

You need a controller that uses @Valid, the annotation that makes Spring check a parameter's constraints before calling the method. The handler below goes in the controller itself, or in a class annotated @RestControllerAdvice, which applies its handlers to every controller.

From a failed validation to a field-by-field response

Catch MethodArgumentNotValidException

java
@ExceptionHandler(MethodArgumentNotValidException.class)
ResponseEntity<Map<String, String>> handleValidation(MethodArgumentNotValidException ex) {

@ExceptionHandler tells Spring to call this method whenever that exception is thrown while handling a request, instead of answering with its default 400.

Read the field errors out of its BindingResult

java
    Map<String, String> errors = new HashMap<>();
    for (var error : ex.getBindingResult().getFieldErrors()) {
        errors.put(error.getField(), error.getDefaultMessage());
    }

The exception's BindingResult holds the outcome of the validation. getFieldErrors() returns one entry per broken rule, each with the field's name and the message from its annotation.

Return them with a 400

java
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}

The map of field names to messages becomes the JSON body, and the status stays 400 Bad Request, because the client sent something wrong.

The response for a request with an empty pickup and zero passengers:

text
{"passengers":"Passengers must be at least 1","pickup":"Pickup location is required"}
run in a container: the real response body, built from the real field errors

Every message came from the message attribute on RideRequest's constraint annotations. You know it worked when an invalid request returns this map instead of an empty 400: a client reading it knows exactly which two fields to fix.

One field can break two rules at once, such as a pickup that is both blank and too short. put keeps only the last message for each field, which is usually enough; if a client needs every message, collect a list per field instead. So the handler decides how much detail the client gets, and writing it once in a @RestControllerAdvice gives every endpoint the same error format.