Sitemap

From a Long Spring Boot Stack Trace to Clean API Error Responses.

6 min readOct 27, 2026

--

Understanding @Valid, @RestControllerAdvice, and @ExceptionHandler by building a global validation error handler.

Press enter or click to view image in full size

The problem that started it

While learning Spring Boot and building a Patient Service REST API, I ran into a problem that many backend developers eventually encounter: validation errors.

I sent a POST request to /patients using Postman. The request contained patient details, but one of the fields failed validation.

Spring Boot returned a 400 Bad Request, along with a detailed error response containing the exception type, stack trace, rejected value, and validation message.

The request had failed for a valid reason. The problem was that the response was difficult to read when I only wanted to understand which field was wrong and how to correct it.

That led me to explore a few important questions:

  • How does Spring Boot detect invalid request data?
  • What exactly happens when @Valid fails?
  • How can I handle validation errors centrally instead of writing the same logic in every controller?
  • How can I return a clean, consistent JSON response?

The answer involves three concepts: DTO validation, exception handling, and HTTP responses.

1. Understanding how request validation works

Consider a simplified patient request DTO:

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class PatientRequestDTO {    @NotBlank(message = "Name is required")
@Size(
max = 100,
message = "Name cannot exceed 100 characters"
)
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Please provide a valid email address")
private String email;
// Getters and setters
}

The annotations define the rules that incoming data must satisfy.

  • @NotBlank rejects null, empty, and whitespace-only strings.
  • @Size(max = 100) limits the length of the name.
  • @Email checks whether the value satisfies the email validator.

One small but important lesson I learned while debugging: @Min and @Max are numeric constraints. They are not the right way to limit the length of a String. For that, use @Size.

These rules describe what valid data should look like. But how does Spring apply them to an incoming HTTP request?

2. What happens when a POST request arrives?

A controller method can look like this:

@PostMapping
public ResponseEntity<PatientResponseDTO> createPatient(
@Valid @RequestBody PatientRequestDTO request) {
    PatientResponseDTO response =
patientService.createPatient(request);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(response);
}

Two annotations are doing different jobs here.

@RequestBody

This tells Spring MVC to read the HTTP request body and deserialize the incoming JSON into a PatientRequestDTO object.

For example:

{
"name": "Rahul Sharma",
"email": "not-an-email"
}

Spring maps the JSON values to the corresponding DTO properties.

@Valid

This triggers validation of the DTO using its configured validation constraints.

In this example, the email value fails the @Email constraint.

For an ordinary @Valid @RequestBody validation failure, Spring MVC raises a MethodArgumentNotValidException and normally returns HTTP 400 Bad Request if the exception is not handled differently.

The controller method body does not proceed to create the patient when this argument validation fails.

This is the first important distinction:

@RequestBody maps the JSON. @Valid checks the resulting object.

3. Introducing global exception handling

Instead of handling validation failures separately in every controller, Spring provides centralized exception handling through @ControllerAdvice and @RestControllerAdvice.

For a REST API, I can use @RestControllerAdvice because it combines controller-advice behavior with response-body serialization.

Here is a simple global validation handler:

import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>>
handleValidationException(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new LinkedHashMap<>(); ex.getBindingResult()
.getFieldErrors()
.forEach(error ->
errors.putIfAbsent(
error.getField(),
error.getDefaultMessage()
)
);
return ResponseEntity
.badRequest()
.body(errors);
}
}

Let’s understand why this works.

@RestControllerAdvice

This allows the exception-handling method to apply across controllers, rather than requiring the same handler in every controller class.

@ExceptionHandler

This tells Spring which exception type the method handles.

Get Aryan Chauhan’s stories in your inbox

Join Medium for free to get updates from this writer.

Here, the handler targets MethodArgumentNotValidException, which commonly arises from validation of a @Valid @RequestBody DTO.

It does not automatically handle every possible application error.

ex.getBindingResult().getFieldErrors()

The exception contains information about the validation failures.

getFieldErrors() returns the field-level errors, including details such as the field name and the associated validation message.

For example, one error may identify the email field and contain the message Please provide a valid email address.

forEach() and the lambda expression

This part is worth understanding:

.forEach(error ->
errors.putIfAbsent(
error.getField(),
error.getDefaultMessage()
)
);

forEach() iterates over the list of field errors.

The lambda expression error -> ... tells Java what to do with each error.

For every field error:

  • error.getField() retrieves the field name.
  • error.getDefaultMessage() retrieves the configured validation message.
  • errors.putIfAbsent() stores the message against that field if no message has been stored for it yet.

I could also write this using a traditional for loop. The lambda is a shorter way of expressing the same operation.

I used LinkedHashMap to retain insertion order, which makes the response easier to read. This is useful for presentation, although clients should not depend on JSON property order.

One limitation is that a Map<String, String> stores only one message per field. If I want to return every validation message for a field that violates several constraints, I would use a structure such as Map<String, List<String>> instead.

4. What does the client receive now?

For the invalid request:

{
"name": "Rahul Sharma",
"email": "not-an-email"
}

The handler can return a response like this:

{
"email": "Please provide a valid email address"
}

The HTTP status remains:

400 Bad Request

The important improvement is not that the invalid request is suddenly accepted. It is that the API communicates the validation failure in a concise, predictable format.

A frontend can use the field name to associate the error with the appropriate input. A developer testing the endpoint in Postman can quickly identify what needs to change.

The exact response depends on the validation constraints that fail and on how the handler is implemented.

5. Why centralize exception handling?

Imagine an application with patient, doctor, appointment, and billing controllers.

Without centralized handling, each controller might end up implementing similar error-response logic. Over time, those responses can become inconsistent.

A global handler provides a common place to define how selected exceptions are represented in HTTP responses.

The benefits include:

  • Consistency: Multiple controllers can follow the same response format.
  • Maintainability: Shared handling logic lives in one place.
  • Readability: Controllers can focus on request handling and application logic.
  • Better API usability: Clients receive useful error information without having to interpret a long stack trace.

There is also an important security consideration. Detailed exception traces are useful during development, but they should not be exposed indiscriminately to public API clients. Internal diagnostics belong in server-side logs, while client responses should reveal only the details appropriate for that API.

6. A few things this handler does not solve

A global validation handler is a useful starting point, not a complete error-handling strategy.

Malformed JSON is a different problem. If the request body contains invalid JSON syntax or cannot be deserialized into the expected Java types, Spring may raise HttpMessageNotReadableException instead. That requires separate handling if a custom response is desired.

Business errors need their own treatment. For example, attempting to create a patient with an email that already exists is a business or persistence rule, not necessarily a DTO validation error.

Multiple validation messages require a deliberate response design. The simple map in this example preserves only one message per field. More complete APIs can return a list of messages or adopt a standardized error structure.

Understanding which exception occurred is just as important as writing the handler itself.

7. What I took away from this

The biggest lesson for me was that request validation and exception handling are two separate stages.

Validation identifies invalid input. Spring raises an exception when the relevant validation fails. The global handler then decides how to present that failure to the API client.

I also learned to read an error response methodically: look at the HTTP status, identify the failing field, inspect the rejected value, and find the constraint that failed before changing the code.

That approach helped me understand Spring Boot beyond simply getting an endpoint to work.

As I continue developing backend applications, I want to focus on not only handling successful requests, but also making failures clear, consistent, and easier to debug.

A useful API is not just one that works when everything goes right. It should also communicate clearly when something goes wrong.

References

--

--

Aryan Chauhan
Aryan Chauhan

Written by Aryan Chauhan

Software developer documenting my journey through Java, Spring Boot, backend engineering, DSA, and system design. Sharing practical lessons