REST API Validation
👉 Master Spring Boot Property, Parameter, and Return Type validation in controllers.
What is API validation ?
API Validation is the process of ensuring that the data sent by a client (like a mobile app or a web browser) meets specific criteria before your application logic processes it.
Think of it as a security guard at the entrance of the application. If a user tries to create an account but forgets to include an email, or provides a password that is too short, validation catches these errors immediately and sends a clear message back to the user.
Maven Dependency
To enable automatic validation support in Spring Boot REST controllers and service layers, you need the following starter dependency. It bundles the required validation API and implementation (like Hibernate Validator).
| Key Point | Detail |
|---|---|
| Purpose | Integrates Jakarta Bean Validation with Spring Boot. |
| Artifact | spring-boot-starter-validation |
| Usage | Essential for annotations like @Valid, @Size, @Past, etc. |
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Validation
Validation in a Spring Boot REST API typically involves using the @Valid annotation on a method parameter to trigger validation on the request body (DTO/Bean).
Define validation for Controller Request
The @Valid annotation on the @RequestBody parameter tells Spring to check the constraints (like @Size, @Past) defined inside the User class before executing the method logic. If validation fails, Spring throws a MethodArgumentNotValidException, which is typically handled globally by a Spring Boot application to return a 400 Bad Request error.
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.RestController;
// ... other imports ...
@RestController
public class UserController {
// ... service and logger ...
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
logger.debug("User to save : {}", user);
var savedUser = userDaoService.save(user);
var location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("{id}")
.buildAndExpand(savedUser.getId())
.toUri();
return ResponseEntity.created(location).body(savedUser);
}
}
Validataion on Model properties
The constraints themselves are defined within the User POJO using annotations from the jakarta.validation.constraints.* package.
| Annotation | Purpose |
|---|---|
@Size | Checks if the annotated element’s size (for Strings, Collections, Maps) is between the specified boundaries. |
@Past | Checks if the annotated element (Date, Calendar, or Joda Time equivalents) is a date in the past. |
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Size;
import java.time.LocalDate;
public class User {
private Integer id;
@Size(min = 3, max = 20, message = "Name must be more than 2 characters.")
private String name;
@Past(message = "Birth date should be in past.")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private LocalDate birthDate;
// constructors, setter-getters and other methods.
}
Project Reference:
Check the project POC on Github - Property Validation.
Validation Scope
The Jakarta Bean Validation API, leveraged by Spring, allows validation on different scopes: Property, Method Parameter, and Method Return Type.
1. Method Parameter Validation (Most Common)
- Use Case: Validating the body of a POST/PUT request (as demonstrated above), or a path/query parameter.
- Mechanism: Use
@Validon the@RequestBodyor use constraint annotations directly on method parameters (e.g.,@Minon a query parameter).
2. Property Validation (Demonstrated Above)
- Use Case: Defining constraints on the fields of a bean (DTO/Model).
- Mechanism: Direct use of constraint annotations (e.g.,
@Size,@NotNull,@Email) on fields within the class. This is the source of the rules.
3. Method Return Type Validation
- Use Case: Validating the object returned by a method. Useful for ensuring that data retrieved from a database or another service adheres to constraints before being sent to the client. Requires using
@Validatedat the class level. - Limitation: Less common and can add performance overhead.
4. Property Validation (Cascading)
- Use Case: When a DTO/Model contains another DTO/Model, and you want to validate the constraints on the nested object as well.
- Mechanism: Place
@Validon the nested property within the parent class.
Validation Request Flow
The following diagram illustrates the standard validation flow for a REST API call.
sequenceDiagram
participant C as Client
participant SB as Spring Boot App
participant V as Validator (Jakarta)
C->>SB: POST /users (Request Body)
SB->>SB: Pre-check for @Valid on parameter
alt Validation Required
SB->>V: Validate User Object
V->>V: Check @Size, @Past constraints
alt Validation Fails
V-->>SB: ConstraintViolations
SB->>SB: Handle MethodArgumentNotValidException
SB-->>C: 400 Bad Request (Error Details)
else Validation Success
V-->>SB: Validation Success
SB->>SB: Execute Controller Method
SB->>SB: Service Logic
SB-->>C: 201 Created (User)
end
else No @Valid
SB->>SB: Execute Controller Method
SB->>SB: Service Logic
SB-->>C: Success Response
end
Global Exception Handling
When a client sends an invalid request body to a Spring Boot controller annotated with @Valid, the framework throws a MethodArgumentNotValidException. We use a Global Exception Handler component (annotated with @ControllerAdvice) to intercept this exception and transform it into a clear, structured JSON response.
Exception Handler Component
We define a class annotated with @ControllerAdvice to make it applicable across all controllers. The key method uses @ExceptionHandler(MethodArgumentNotValidException.class) to specifically handle validation failures.
- Key Action: Iterates through all field errors present in the exception object.
- Response Status: Uses
@ResponseStatus(HttpStatus.BAD_REQUEST)to ensure the HTTP status code is correctly set to400. - Response Body: Formats the errors into a map, typically mapping the field name to the error message, which is highly readable by API consumers.
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
public class CustomizedResponseEntityExceptionHandler {
/**
* Handles exceptions thrown when method argument validation fails (e.g., @Valid on @RequestBody).
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
// Add Global Errors if any (non-field specific)
ex.getBindingResult().getGlobalErrors().forEach(error -> {
errors.put(error.getObjectName(), error.getDefaultMessage());
});
return errors;
}
}
Testing Exception handler
The handler ensures a clean, consistent response when the input fails the defined constraints.
Example Client Request (Invalid Data)
If a client sends a request that violates the @Size and @Past constraints defined in the User bean:
{
"name": "A",
"birthDate": "2026-01-01"
}
Response (Processed by @ControllerAdvice)
Instead of default bulky HTTP 500 error, the client receives a structured 400 Bad Request with specific field issues:
{
"name": "Name must be more than 2 characters.",
"birthDate": "Birth date should be in past."
}
Request Flow
This approach centralizes error handling, adhering to the Separation of Concerns principle. The controller focuses purely on business logic, and the advice component handles the cross-cutting concern of exception management.
sequenceDiagram
participant C as Client
participant Ctrl as Controller (@PostMapping)
participant EH as ExceptionHandler (@ControllerAdvice)
C->>Ctrl: Invalid POST Request (User DTO)
Ctrl->>Ctrl: Validation Triggered by @Valid
alt Validation Fails
Ctrl->>EH: throws MethodArgumentNotValidException
EH->>EH: @ExceptionHandler catches Exception
EH->>EH: Extracts Field Errors
EH-->>C: 400 Bad Request (JSON Error Map)
else Validation Success
Ctrl->>Ctrl: Business Logic Executes
Ctrl-->>C: 201 Created
end
Note:
To standaradise the Error response across APIs, you can create a custom Error DTO (Data Transfer Object) instead of a simpleMap<String, String>and include more contextual information, such as a timestamp, a unique error code, and the request path.
Best Practices
| Area | Suggestion | Detail |
|---|---|---|
| Error Handling | Implement a Global Exception Handler. | Use @ControllerAdvice and @ExceptionHandler to uniformly catch MethodArgumentNotValidException and format the validation errors into a clean JSON response (e.g., listing all field errors) for better UX. |
| Group Validation | Use Validation Groups. | For complex models, use groups (marker interfaces) to selectively apply validation constraints (e.g., different rules for Create vs. Update operations). Use @Validated(OnCreation.class) instead of just @Valid. |
| Custom Constraints | Create Custom Annotations. | For business-specific validation (e.g., “Must be an existing product ID”), implement a custom constraint with @Constraint and a corresponding ConstraintValidator. |
| DRY Principle | Separate DTOs from Entities. | Always validate a Data Transfer Object (DTO) that mirrors the request structure, not the JPA Entity itself, to keep persistence logic clean from presentation concerns. |
Note:
Spring Boot internally uses the Hibernate Validator as the default implementation of the Jakarta Bean Validation API, where validation annotations are available injakarta.validation.constraints.*package.