Spring REST Validation
In this tutorial, we'll explore how to implement validation in Spring REST APIs. Validation is an essential aspect of building robust applications as it helps ensure that data received by your application meets expected criteria before processing.
Introduction to REST Validation
When building REST APIs, you need to validate incoming data to:
- Prevent bad data from entering your application
- Provide clear error messages to clients
- Reduce the need for complex error handling in your business logic
- Improve security by rejecting potentially malicious inputs
Spring offers seamless integration with Bean Validation API (JSR-380) through Hibernate Validator, the reference implementation of this specification.
Setting Up Validation Dependencies
To use validation in your Spring Boot application, you need to include the Spring Boot starter validation dependency:
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
Basic Validation Annotations
Spring REST validation uses annotations to define constraints. Here are some common validation annotations:
Annotation | Description |
---|---|
@NotNull | Validates that the value is not null |
@NotEmpty | Validates that the value is not null or empty |
@NotBlank | Validates that the string is not null and contains at least one non-whitespace character |
@Size | Validates that the size of a collection, array, or string is within a specified range |
@Min | Validates that the number is greater than or equal to a specified value |
@Max | Validates that the number is less than or equal to a specified value |
@Email | Validates that the string is a valid email address |
@Pattern | Validates that the string matches a regular expression |
Implementing Validation in REST Controllers
Let's implement validation in a Spring REST controller using a practical example:
1. Create a Model Class with Validation Annotations
First, let's create a User
class with validation rules:
package com.example.validation.model;
import jakarta.validation.constraints.*;
import java.time.LocalDate;
public class User {
@NotNull(message = "ID cannot be null")
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@NotNull(message = "Date of birth is required")
@Past(message = "Date of birth must be in the past")
private LocalDate dateOfBirth;
@Min(value = 18, message = "Age must be at least 18")
private int age;
// Getters and Setters
// Constructor(s)
}
2. Create a REST Controller with Validation
Now, let's create a controller that validates incoming requests:
package com.example.validation.controller;
import com.example.validation.model.User;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
@PostMapping
public ResponseEntity<Object> createUser(@Valid @RequestBody User user) {
// In a real application, you would save the user to a database
Map<String, Object> response = new HashMap<>();
response.put("message", "User created successfully");
response.put("user", user);
return new ResponseEntity<>(response, HttpStatus.CREATED);
}
@PutMapping("/{id}")
public ResponseEntity<Object> updateUser(
@PathVariable Long id,
@Valid @RequestBody User user) {
// Check if IDs match
if (!id.equals(user.getId())) {
return new ResponseEntity<>("IDs don't match", HttpStatus.BAD_REQUEST);
}
// In a real application, you would update the user in the database
Map<String, Object> response = new HashMap<>();
response.put("message", "User updated successfully");
response.put("user", user);
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
Notice the key annotations:
@Valid
- Tells Spring to validate the annotated request body@Validated
- Activates method parameter validation for the controller class
Handling Validation Errors
When validation fails, Spring throws MethodArgumentNotValidException
. Let's create a global exception handler to provide user-friendly error responses:
package com.example.validation.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
Map<String, Object> response = new HashMap<>();
response.put("message", "Validation failed");
response.put("errors", errors);
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}
}
Testing Our Validation
Now, let's test our API with valid and invalid data:
Invalid Request Example
Request:
POST /api/users HTTP/1.1
Host: localhost:8080
Content-Type: application/json
{
"id": 1,
"name": "J",
"email": "invalid-email",
"dateOfBirth": "2025-01-01",
"age": 15
}
Response:
{
"message": "Validation failed",
"errors": {
"name": "Name must be between 2 and 50 characters",
"email": "Email must be valid",
"dateOfBirth": "Date of birth must be in the past",
"age": "Age must be at least 18"
}
}
Valid Request Example
Request:
POST /api/users HTTP/1.1
Host: localhost:8080
Content-Type: application/json
{
"id": 1,
"name": "John Smith",
"email": "[email protected]",
"dateOfBirth": "1990-01-01",
"age": 33
}
Response:
{
"message": "User created successfully",
"user": {
"id": 1,
"name": "John Smith",
"email": "[email protected]",
"dateOfBirth": "1990-01-01",
"age": 33
}
}
Advanced Validation Techniques
Group Validation
Sometimes you need different validation rules for different operations (create vs. update). You can use validation groups:
First, define interfaces for the groups:
package com.example.validation.validation;
public interface ValidationGroups {
interface Create {}
interface Update {}
}
Then specify the groups in your model:
package com.example.validation.model;
import com.example.validation.validation.ValidationGroups.Create;
import com.example.validation.validation.ValidationGroups.Update;
import jakarta.validation.constraints.*;
import java.time.LocalDate;
public class User {
@Null(groups = Create.class, message = "ID must be null for creation")
@NotNull(groups = Update.class, message = "ID cannot be null for update")
private Long id;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
private String name;
// Other fields and methods remain the same
}
Finally, specify the group in your controller:
@PostMapping
public ResponseEntity<Object> createUser(
@Validated(Create.class) @RequestBody User user) {
// Implementation
}
@PutMapping("/{id}")
public ResponseEntity<Object> updateUser(
@PathVariable Long id,
@Validated(Update.class) @RequestBody User user) {
// Implementation
}
Custom Validation
You can also create custom validation annotations:
- Create a validation annotation:
package com.example.validation.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = PasswordConstraintValidator.class)
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPassword {
String message() default "Invalid password";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- Implement the validator:
package com.example.validation.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, String> {
@Override
public void initialize(ValidPassword constraintAnnotation) {
}
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
// Password must be at least 8 characters
// Must contain at least one digit
// Must contain at least one uppercase letter
if (password == null) {
return false;
}
boolean hasLength = password.length() >= 8;
boolean hasDigit = password.matches(".*\\d.*");
boolean hasUppercase = !password.equals(password.toLowerCase());
boolean isValid = hasLength && hasDigit && hasUppercase;
if (!isValid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
"Password must be at least 8 characters, contain a digit and an uppercase letter"
).addConstraintViolation();
}
return isValid;
}
}
- Use the annotation in your model:
@ValidPassword
private String password;
Parameter Validation
You can also validate path variables and request parameters:
@GetMapping("/{id}")
public ResponseEntity<Object> getUser(
@PathVariable @Min(1) Long id) {
// Implementation
}
@GetMapping("/search")
public ResponseEntity<Object> searchUsers(
@RequestParam @NotBlank String keyword) {
// Implementation
}
Best Practices for REST Validation
-
Keep validation logic separate from business logic: Use validation annotations for simple validation and custom validators for complex rules.
-
Provide clear error messages: Always include descriptive messages in validation annotations to help clients understand the issue.
-
Use appropriate HTTP status codes: Use
400 Bad Request
for validation errors and provide detailed error information in the response body. -
Validate at the controller level: Apply validation at the entry point of your application (controllers).
-
Use validation groups: When different operations require different validation rules.
-
Don't trust client-side validation: Always validate on the server side, even if validation is also done on the client.
Summary
In this tutorial, we've covered:
- Setting up Spring REST validation
- Using basic validation annotations
- Implementing validation in REST controllers
- Handling validation errors
- Advanced validation techniques including group validation and custom validators
- Best practices for REST validation
Properly implemented validation improves the quality of your API, reduces bugs, and provides better user experience by providing clear feedback when something goes wrong.
Additional Resources
- Jakarta Bean Validation specification
- Hibernate Validator documentation
- Spring Validation official documentation
Exercises
- Create a
Product
class with appropriate validation for fields likename
,price
, andcategory
. - Implement a REST controller for product management with validation.
- Add custom validation to ensure product codes follow a specific pattern.
- Implement group validation to handle different validation rules for product creation versus updates.
- Create a global exception handler that returns validation errors in a standardized format.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)