Why validation belongs at the API boundary
Imagine an API endpoint that accepts user input without any validation. One day, a client sends a request with a negative amount, an invalid email, and missing required fields. The request still reaches the service layer, corrupts business logic, and causes unexpected errors deep inside the system. The bug is hard to trace, not because the logic is complex, but because invalid data was never stopped at the boundary.
What happens when a request arrives (overview)
When a client sends an HTTP request to a Spring Boot application, a small pipeline runs before your controller code executes. Understanding that pipeline explains where validation belongs and when exceptions are thrown:
-
DispatcherServletreceives the request and resolves a controller handler. - Spring prepares method invocation by resolving method arguments. For
@RequestBody, it uses anHttpMessageConverterto deserialize JSON into the target DTO. - During argument resolution, Spring integrates
Jakarta Bean Validation(Hibernate Validator by default). If a controller parameter is annotated with@Validor the controller is annotated with@Validated, the deserialized DTO is validated automatically. If any constraint violations are found, Spring raises a validation exception and does not invoke the controller method body. -
The thrown exception is then routed through Spring's exception resolution mechanism, where application level handlers decide the HTTP response.
Common validation exceptions you should know
MethodArgumentNotValidException: Raised when validating an@RequestBodyfails (field-level or object-level violations). This exception carries the binding result with field errors and rejected values.ConstraintViolationException: Raised when method parameter validation (@Validatedon parameters,@RequestParam,@PathVariable, or programmatic validator calls) fails. The exception contains a set ofConstraintViolationsdescribing each failure.HttpMessageNotReadableException: Raised when JSON parsing fails (malformed JSON) before validation even runs.
These exceptions are thrown before your controller logic runs. The responsibility for shaping the HTTP response falls to the configured exception handlers.
Below are the simple code snippets that illustrate how validation is declared and enforced.
// Request DTO (contract and annotations)
public class RequestPayload {
@NotBlank
private String name;
@Email
@NotBlank
private String email;
@NotBlank
@DepartmentCodeValid
private String departmentCode;
}
Notes: the DTO declares both standard constraints (@NotBlank, @Email) and the custom domain constraint (@DepartmentCodeValid) directly on the field. This keeps the data contract and validation rules colocated and self documenting.
// Custom constraint annotation
@Documented
@Constraint(validatedBy = DepartmentCodeValidator.class)
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface DepartmentCodeValid {
String message() default "departmentCode must be one of 50..99";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// Custom validator implementation
// this validator constrains that `departmentCode` must be in the
// defined codes
public class DepartmentCodeValidator implements ConstraintValidator<DepartmentCodeValid, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isBlank()) {
return false;
}
try {
int code = Integer.parseInt(value);
for (DepartmentCode dc : DepartmentCode.values()) {
if (dc.getCode() == code) {
return true;
}
}
return false;
} catch (NumberFormatException e) {
return false;
}
}
}
// Domain enumeration of allowed department codes
public enum DepartmentCode {
CODE_50(50),
CODE_98(98),
...
CODE_99(99);
private final int code;
DepartmentCode(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
// Controller that accepts the request
@RestController
@RequestMapping("/api/requests")
public class RequestController {
private final CacheService cacheService;
public RequestController(CacheService cacheService) {
this.cacheService = cacheService;
}
@PostMapping
public ResponseEntity<RequestPayload> create(@Valid @RequestBody RequestPayload payload) {
var saved = cacheService.save(payload);
return ResponseEntity.ok(saved);
}
}
How validation failure is raised and routed (walkthrough)
- When the POST handler receives a request, Spring deserializes the JSON body into
RequestPayload. - Because the create method parameter is annotated with
@Valid, the bean validation engine runs automatically. - The engine executes built-in constraints (
@NotBlank,@Email) and the customDepartmentCodeValidator. - If the custom validator rejects the
departmentCode(or any other constraint fails), the engine records violations and Spring throwsMethodArgumentNotValidException. - Spring's HandlerExceptionResolver chain (including any @RestControllerAdvice) receives that exception and decides how to convert it into an HTTP response. The controller method body is never invoked.
Centralizing validation error handling recommended
Below is an illustrative GlobalExceptionHandler you can adopt in the project. It is shown inline here to demonstrate how the binding errors and constraint violations can be transformed into a stable JSON error envelope.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleBindingErrors(MethodArgumentNotValidException ex,
HttpServletRequest request) {
List<FieldError> errors = ex.getBindingResult().getFieldErrors().stream()
.map(fe -> new FieldError(fe.getField(), fe.getDefaultMessage(), fe.getRejectedValue()))
.collect(Collectors.toList());
ValidationErrorResponse body = new ValidationErrorResponse(
OffsetDateTime.now(), HttpStatus.BAD_REQUEST.value(), request.getRequestURI(), errors
);
return ResponseEntity.badRequest().body(body);
}
// Example: handle type mismatches or malformed values for query/path params
@ExceptionHandler({ MethodArgumentTypeMismatchException.class })
public ResponseEntity<ValidationErrorResponse> handleTypeMismatch(MethodArgumentTypeMismatchException ex,
HttpServletRequest request) {
FieldError err = new FieldError(ex.getName(), "invalid value", ex.getValue());
ValidationErrorResponse body = new ValidationErrorResponse(
OffsetDateTime.now(), HttpStatus.BAD_REQUEST.value(), request.getRequestURI(), List.of(err)
);
return ResponseEntity.badRequest().body(body);
}
// Add other handlers (ConstraintViolationException, HttpMessageNotReadableException, etc.) as needed
}
// Minimal helper DTOs used above
record FieldError(String field, String message, Object rejectedValue){}
record ValidationErrorResponse(OffsetDateTime timestamp, int status, String path, List<FieldError> errors) {}
Notes on the example handle
The handler reads field errors from the binding result and maps them to a compact FieldError structure. This is the most direct way to surface which property failed and why.
The same pattern can be applied to ConstraintViolationException by mapping each ConstraintViolation to a field/property path.
A consistent error envelope returned by the global handler might look like:
{
"timestamp": "2025-12-28T12:34:56Z",
"status": 400,
"path": "/api/requests",
"errors": [
{
"field": "departmentCode",
"message": "departmentCode must be one of 50..99",
"rejectedValue": "42"
}
]
}
How Spring chooses which handler runs (internals, simplified)
When the validation exception is thrown, Spring consults HandlerExceptionResolvers. @RestControllerAdvice backed ExceptionHandlerExceptionResolver is consulted and, if a matching @ExceptionHandler exists, delegates to it. The handler returns a value or ResponseEntity, which Spring serializes to the HTTP response and ends processing.
If no handler is present, Spring's fallback resolvers generate a default 400 response with a framework defined body.
Conclusion
Walk through the article, we can see that validation is the first line of defense for any service. It protects business logic and persistence from malformed or malicious input, improves client experience with clear errors, and reduces debugging time for both providers and consumers. Placing validation at the boundary just after HTTP mapping but before controller logic lets you fail fast and keep deeper layers simple.
Hopefully the artical can help you to more get more understand about the validation workflow and helpful for developer career, thanks youuu... ☺️

Top comments (2)
nice
Some comments may only be visible to logged-in visitors. Sign in to view all comments.