Bean Validation in Spring Boot is a framework for validating the properties of a JavaBean using annotations. It leverages the Java Bean Validation API (JSR 380) and Hibernate Validator to perform validation. It
Ensure data integrity by validating user input at the application layer.
Reduce boilerplate validation logic in your code by relying on declarative annotations.
Integrates seamlessly with Spring Boot's controller and service layers.
Ensures the property is not null or empty (only for Strings and collections).
@NotBlank
Ensures the property is not null or blank (for Strings).
@Size
Restricts the size of a String, collection, array, or map.
@Min / @Max
Sets minimum and maximum values for numeric fields.
@Email
Validates an email format.
@Pattern
Validates against a regular expression.
@Positive / @Negative
Ensures the property is positive or negative.
@Valid
Triggers validation for nested objects.
@Past / @Future
Ensures the property is a date in the past or future.
To validate bean object we can use @Valid in controller or service layer
How to handle exceptions when validation fails?
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
How to validate nested objects?
Use @Valid on nested objects to trigger validation:
public class Order {
@Valid
@NotNull
private User user;
}
How to do groups and conditional validation?
To do group or conditional validation, we can use groups attribute and @Validated .
// first create groups
public interface CreateGroup {}
public interface UpdateGroup {}
// attach group with groups attrubute
public class User {
@NotNull(groups = CreateGroup.class)
private String name;
@NotNull(groups = UpdateGroup.class)
private Integer id;
}
// use @validated with specific group that want to validate
@PostMapping
public ResponseEntity<String> createUser(@Validated(CreateGroup.class) @RequestBody User user) {
return ResponseEntity.ok("User created");
}
How to do custom validation of beans?
To do custom validation of beans we can use validation factory and the validator class
public class BeanValidator {
private static final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
private static final Validator validator = factory.getValidator();
public static <T> void validate(T bean) {
Set<ConstraintViolation<T>> violations = validator.validate(bean);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<T> violation : violations) {
sb.append(violation.getPropertyPath()).append(": ").append(violation.getMessage()).append("\n");
}
throw new IllegalArgumentException("Bean validation failed:\n" + sb.toString());
}
}
}
How to create own custom validation constraints?
To create our own custom validation constraints we need to create a custom annotation a validator class by extending ConstraintValidator.
Below there is an example of custom constraint for validate file.
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.web.multipart.MultipartFile;
import java.util.Arrays;
import java.util.List;
/**
* Validator class for @ValidFile annotation, used to validate file size and extension
* @see ValidFile
* @Author saurabh vaish
* @Date 04-09-2024
*/
public class FileValidator implements ConstraintValidator<ValidFile, Object> {
private long maxSizeBytes;
private List<String> allowedExtensions;
@Override
public void initialize(ValidFile constraintAnnotation) {
this.maxSizeBytes = constraintAnnotation.maxSizeInMb() * 1024 * 1024;
this.allowedExtensions = Arrays.asList(constraintAnnotation.allowedExtensions());
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
return switch (value) {
case null -> true; // null values are validated with @NotNull
case MultipartFile multipartFile -> validateFile(multipartFile, context);
case MultipartFile[] multipartFiles -> validateFiles(List.of(multipartFiles), context);
case List<?> list -> validateFiles((List<MultipartFile>) list, context);
default -> false;
};
}
private boolean validateFile(MultipartFile file, ConstraintValidatorContext context) {
if (file.isEmpty()) {
return true; // Empty files are allowed, use @NotNull if you want to enforce file presence
}
String fileName = file.getOriginalFilename();
if (fileName == null || fileName.contains("..")) {
addConstraintViolation(context, "Invalid file name");
return false;
}
String extension = getFileExtension(fileName);
if (!allowedExtensions.contains(extension.toLowerCase())) {
addConstraintViolation(context, "Invalid file type. Allowed types are: " + String.join(", ", allowedExtensions));
return false;
}
if (file.getSize() > maxSizeBytes) {
addConstraintViolation(context, "File size exceeds the maximum allowed size of " + (maxSizeBytes / 1024 / 1024) + "MB");
return false;
}
return true;
}
private boolean validateFiles(List<MultipartFile> files, ConstraintValidatorContext context) {
for (MultipartFile file : files) {
if (!validateFile(file, context)) {
return false;
}
}
return true;
}
private void addConstraintViolation(ConstraintValidatorContext context, String message) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
}
private String getFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf('.');
return (dotIndex == -1) ? "" : fileName.substring(dotIndex + 1);
}
}
Use of this custom constraint
@NotNull(message = "Image file is required if imageUrl is not provided")
@Schema(description = "Image file (optional if imageUrl is provided)", type = "string", format = "binary")
@ValidFile(message = "Invalid file. Only JPG, JPEG, and PNG files up to 5MB are allowed.", maxSizeInMb = 5,allowedExtensions = {"jpg", "jpeg", "png"})
MultipartFile file
Handle Exceptions Using Problem Detail
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidationExceptions(MethodArgumentNotValidException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problemDetail.setTitle("Validation Error");
problemDetail.setDetail("Invalid input provided");
ex.getBindingResult().getFieldErrors().forEach(error ->
problemDetail.setProperty(error.getField(), error.getDefaultMessage())
);
return problemDetail;
}
}
// reponse
{
"type": "about:blank",
"title": "Validation Error",
"status": 400,
"detail": "Invalid input provided",
"instance": "/api/users",
"name": "Name must not be blank",
"email": "Email must be a valid address"
}