While Spring standard annotations (@NotBlank, @NotNull, @Min, @Size, etc.) cover many use cases when validating user input, there are times when we need to create custom validation logic for a more specific type of input. In this article, I will demonstrate how to create custom annotations for validation.
We need to add the spring-boot-starter-validation dependency to our pom.xml file.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
Let’s create custom annotations to validate file attributes, such as file extension, file size, and MIME type.
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint( validatedBy = {FileExtensionValidator.class} ) public @interface ValidFileExtension { String[] extensions() default {}; String message() default "{constraints.ValidFileExtension.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint( validatedBy = {FileMaxSizeValidator.class} ) public @interface ValidFileMaxSize { long maxSize() default Long.MAX_VALUE; // MB String message() default "{constraints.ValidFileMaxSize.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
@Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint( validatedBy = {FileMimeTypeValidator.class} ) public @interface ValidFileMimeType { String[] mimeTypes() default {}; String message() default "{constraints.ValidFileMimeType.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
Let's break down these annotations' components:
public class FileExtensionValidator implements ConstraintValidator<ValidFileExtension, MultipartFile> { private List<String> extensions; @Override public void initialize(ValidFileExtension constraintAnnotation) { extensions = List.of(constraintAnnotation.extensions()); } @Override public boolean isValid(MultipartFile file, ConstraintValidatorContext constraintValidatorContext) { if (file == null || file.isEmpty()) { return true; } var extension = FilenameUtils.getExtension(file.getOriginalFilename()); return StringUtils.isNotBlank(extension) && extensions.contains(extension.toLowerCase()); } }
public class FileMaxSizeValidator implements ConstraintValidator<ValidFileMaxSize, MultipartFile> { private long maxSizeInBytes; @Override public void initialize(ValidFileMaxSize constraintAnnotation) { maxSizeInBytes = constraintAnnotation.maxSize() * 1024 * 1024; } @Override public boolean isValid(MultipartFile file, ConstraintValidatorContext constraintValidatorContext) { return file == null || file.isEmpty() || file.getSize() <= maxSizeInBytes; } }
@RequiredArgsConstructor public class FileMimeTypeValidator implements ConstraintValidator<ValidFileMimeType, MultipartFile> { private final Tika tika; private List<String> mimeTypes; @Override public void initialize(ValidFileMimeType constraintAnnotation) { mimeTypes = List.of(constraintAnnotation.mimeTypes()); } @SneakyThrows @Override public boolean isValid(MultipartFile file, ConstraintValidatorContext constraintValidatorContext) { if (file == null || file.isEmpty()) { return true; } var detect = tika.detect(TikaInputStream.get(file.getInputStream())); return mimeTypes.contains(detect); } }
These classes are implementations of the ConstraintValidator interface and contain the actual validation logic.
For FileMimeTypeValidator, we will use Apache Tika (a toolkit designed to extract metadata and content from numerous types of documents).
Let's create a TestUploadRequest class intended for handling file uploads, specifically for a PDF file.
@Data public class TestUploadRequest { @NotNull @ValidFileMaxSize(maxSize = 10) @ValidFileExtension(extensions = {"pdf"}) @ValidFileMimeType(mimeTypes = {"application/pdf"}) private MultipartFile pdfFile; }
@RestController @Validated @RequestMapping("/test") public class TestController { @PostMapping(value = "/upload", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) public ResponseEntity<String> testUpload(@Valid @ModelAttribute TestUploadRequest request) { return ResponseEntity.ok("test upload"); } }
A custom validation annotation can also be defined at the class level to validate a combination of fields within a class.
Let’s create @PasswordMatches annotation to ensure that two password fields match in a class.
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint( validatedBy = {PasswordMatchesValidator.class} ) public @interface PasswordMatches { String message() default "{constraints.PasswordMatches.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
public interface PasswordDto { String getPassword(); String getConfirmPassword(); }
public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, PasswordDto> { @Override public boolean isValid(PasswordDto password, ConstraintValidatorContext constraintValidatorContext) { return StringUtils.equals(password.getPassword(), password.getConfirmPassword()); } }
The PasswordDto interface is an interface for objects that contain a password and a confirm password field.
The PasswordMatchesValidator class implements the ConstraintValidator interface and contains the logic for validating that the password and confirm password fields match.
Let's create a RegisterAccountRequest class intended for handling user registration data.
@PasswordMatches @Data public class RegisterAccountRequest implements PasswordDto { @NotBlank private String username; @NotBlank @Email private String email; @NotBlank @ToString.Exclude private String password; @NotBlank @ToString.Exclude private String confirmPassword; }
@RestController @Validated @RequestMapping("/auth") public class AuthController { @PostMapping("/register") public ResponseEntity<String> register(@RequestBody @Valid RegisterAccountRequest request) { return ResponseEntity.ok("register success"); } }
In this short article, we discovered how easy it is to create custom annotations to verify a field or class. The code from this article is available over on my Github.
The above is the detailed content of Creating Custom Annotations for Validation in Spring Boot. For more information, please follow other related articles on the PHP Chinese website!