6.3 Custom Validator Implementation
The basic annotations provided by default (like @NotBlank, @Min, etc.) may not be enough to validate all complex business rules of a service. For example, conditions like "the start date must be before the end date" or "specific words must not be included" are solved by creating custom annotations and Validators.
1. Creating a Custom Annotation
First, use @interface to create your own validation annotation. Let's take an example of a @StrongPassword annotation that checks if a password contains certain special characters.
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = StrongPasswordValidator.class) // Specify the class to perform the validation logic
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface StrongPassword {
String message() default "Password must contain at least one uppercase letter, lowercase letter, number, and special character.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
An annotation for Bean Validation must have three essential properties: message, groups, and payload.
2. Implementing Validator Logic
Implement the StrongPasswordValidator format specified with validatedBy above. It must implement the ConstraintValidator interface.
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> {
@Override
public void initialize(StrongPassword constraintAnnotation) {
// Write initialization logic if needed
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isBlank()) {
return false;
}
// Strong password validation logic using regular expressions
boolean hasUppercase = value.matches(".*[A-Z].*");
boolean hasLowercase = value.matches(".*[a-z].*");
boolean hasNumber = value.matches(".*\\d.*");
boolean hasSpecialChar = value.matches(".*[!@#$%^&*()].*");
return hasUppercase && hasLowercase && hasNumber && hasSpecialChar;
}
}
3. Applying the Validator
Now you can naturally attach the created annotation to a DTO field to use it.
public class UserRegisterDto {
@NotBlank
private String username;
@StrongPassword
private String password;
}
By utilizing custom Validators in this manner, you can neatly add flexible and reusable domain-specific logic while maintaining the core validation features provided by the framework.