Custom validator annotations let you check if data meets your own rules. This helps keep your app data clean and correct.
Custom validator annotation in Spring Boot
import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.*; @Documented @Constraint(validatedBy = YourValidatorClass.class) @Target({ ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface YourAnnotation { String message() default "Validation failed message"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
The @Constraint annotation links your custom annotation to its validator class.
The message is the error shown if validation fails.
@NotEmptyString to check if a string is not empty.import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.*; @Documented @Constraint(validatedBy = NotEmptyStringValidator.class) @Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface NotEmptyString { String message() default "String must not be empty"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; public class NotEmptyStringValidator implements ConstraintValidator<NotEmptyString, String> { @Override public boolean isValid(String value, ConstraintValidatorContext context) { return value != null && !value.trim().isEmpty(); } }
This example creates a custom annotation @StartsWithA that checks if a string starts with 'A'. The Person class uses it on the name field. The main method validates two persons and prints violations.
import jakarta.validation.Constraint; import jakarta.validation.Payload; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; import jakarta.validation.constraints.NotNull; import java.lang.annotation.*; import java.util.Set; import jakarta.validation.ConstraintViolation; @Documented @Constraint(validatedBy = StartsWithAValidator.class) @Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) @interface StartsWithA { String message() default "Must start with letter A"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } class StartsWithAValidator implements ConstraintValidator<StartsWithA, String> { @Override public boolean isValid(String value, ConstraintValidatorContext context) { return value != null && value.startsWith("A"); } } class Person { @NotNull @StartsWithA String name; Person(String name) { this.name = name; } } public class CustomValidatorDemo { public static void main(String[] args) { ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); Person p1 = new Person("Alice"); Person p2 = new Person("Bob"); Set<ConstraintViolation<Person>> violations1 = validator.validate(p1); Set<ConstraintViolation<Person>> violations2 = validator.validate(p2); System.out.println("Violations for p1: " + violations1.size()); System.out.println("Violations for p2: " + violations2.size()); for (var v : violations2) { System.out.println(v.getPropertyPath() + ": " + v.getMessage()); } } }
Always implement ConstraintValidator with your annotation and the type you want to validate.
Use @Target to specify where your annotation can be used (fields, methods, etc.).
Remember to add @Retention(RetentionPolicy.RUNTIME) so the annotation is available at runtime.
Custom validator annotations let you create your own rules for data checks.
You link the annotation to a validator class that contains the logic.
Use them to keep validation clean and reusable across your app.