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.
To do group or conditional validation, we can use groups attribute and @Validated .
// first create groupspublicinterfaceCreateGroup {}publicinterfaceUpdateGroup {}// attach group with groups attrubutepublicclassUser { @NotNull(groups =CreateGroup.class)privateString name; @NotNull(groups =UpdateGroup.class)privateInteger id;}// use @validated with specific group that want to validate@PostMappingpublicResponseEntity<String>createUser(@Validated(CreateGroup.class) @RequestBodyUser user) {returnResponseEntity.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
importjakarta.validation.ConstraintValidator;importjakarta.validation.ConstraintValidatorContext;importorg.springframework.web.multipart.MultipartFile;importjava.util.Arrays;importjava.util.List;/** * Validator class for @ValidFile annotation, used to validate file size and extension * @see ValidFile * @Author saurabh vaish * @Date 04-09-2024 */publicclassFileValidatorimplementsConstraintValidator<ValidFile,Object> {privatelong maxSizeBytes;privateList<String> allowedExtensions; @Overridepublicvoidinitialize(ValidFile constraintAnnotation) {this.maxSizeBytes=constraintAnnotation.maxSizeInMb() *1024*1024;this.allowedExtensions=Arrays.asList(constraintAnnotation.allowedExtensions()); } @OverridepublicbooleanisValid(Object value,ConstraintValidatorContext context) {returnswitch (value) {casenull->true; // null values are validated with @NotNullcaseMultipartFile multipartFile ->validateFile(multipartFile, context);caseMultipartFile[] multipartFiles ->validateFiles(List.of(multipartFiles), context);caseList<?> list ->validateFiles((List<MultipartFile>) list, context);default->false; }; }privatebooleanvalidateFile(MultipartFile file,ConstraintValidatorContext context) {if (file.isEmpty()) {returntrue; // 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");returnfalse; }String extension =getFileExtension(fileName);if (!allowedExtensions.contains(extension.toLowerCase())) {addConstraintViolation(context,"Invalid file type. Allowed types are: "+String.join(", ", allowedExtensions));returnfalse; }if (file.getSize() > maxSizeBytes) {addConstraintViolation(context,"File size exceeds the maximum allowed size of "+ (maxSizeBytes /1024/1024) +"MB");returnfalse; }returntrue; }privatebooleanvalidateFiles(List<MultipartFile> files,ConstraintValidatorContext context) {for (MultipartFile file : files) {if (!validateFile(file, context)) {returnfalse; } }returntrue; }privatevoidaddConstraintViolation(ConstraintValidatorContext context,String message) {context.disableDefaultConstraintViolation();context.buildConstraintViolationWithTemplate(message).addConstraintViolation(); }privateStringgetFileExtension(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
@RestControllerAdvicepublicclassGlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class)publicProblemDetailhandleValidationExceptions(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"}