Custom Constraint Annotations
Create a custom @UniqueEmail annotation with a ConstraintValidator implementation.
Custom Constraint Annotations is a free Java Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Java Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
When Standard Constraints Are Not Enough
Standard Bean Validation annotations cover common cases. For domain-specific rules (e.g., unique email, valid IBAN, password strength), create custom constraint annotations.
Defining the Constraint Annotation
Create an annotation with @Constraint(validatedBy = ...), required message, groups, and payload elements, and the appropriate retention/target.
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@Documented
@Constraint(validatedBy = UniqueEmailValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueEmail {
String message() default "Email already registered";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}Implementing ConstraintValidator
Implement ConstraintValidator<UniqueEmail, String>. The initialize method reads annotation elements; isValid performs the actual check.
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
@Autowired private UserRepository repo;
@Override
public boolean isValid(String email, ConstraintValidatorContext ctx) {
if (email == null) return true; // let @NotBlank handle null
return !repo.existsByEmail(email);
}
}Using the Custom Constraint
Apply the annotation like any standard constraint on a field, parameter, or return value.
public record CreateUserRequest(
@NotBlank
String name,
@NotBlank @Email @UniqueEmail
String email
) {}Custom Message with Interpolation
Reference annotation elements in the message using {elementName}. Use ctx.buildConstraintViolationWithTemplate() to add custom messages with dynamic data.
@Constraint(validatedBy = RangeValidator.class)
public @interface InRange {
int min() default 0;
int max() default 100;
String message() default "Must be between {min} and {max}";
// ...
}Class-Level Constraint
Apply a constraint at the class level to validate multiple fields together — useful for cross-field rules like password confirmation.
@Documented
@Constraint(validatedBy = PasswordMatchValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PasswordMatch {
String message() default "Passwords do not match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}Class-Level Validator Implementation
The validator receives the whole object. Access multiple fields to perform cross-field validation.
public class PasswordMatchValidator implements ConstraintValidator<PasswordMatch, ChangePasswordRequest> {
public boolean isValid(ChangePasswordRequest req, ConstraintValidatorContext ctx) {
if (req.getNewPassword() == null) return true;
boolean match = req.getNewPassword().equals(req.getConfirmPassword());
if (!match) {
ctx.disableDefaultConstraintViolation();
ctx.buildConstraintViolationWithTemplate("Passwords do not match")
.addPropertyNode("confirmPassword").addConstraintViolation();
}
return match;
}
}Composing Constraints
Create a meta-constraint by annotating your custom annotation with existing constraints. Both are applied automatically.
@NotBlank
@Email
@Size(max = 255)
@Constraint(validatedBy = {})
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidEmail {
String message() default "Invalid email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}Returning Multiple Violations
Use ctx.disableDefaultConstraintViolation() and add individual violation messages for each issue found in a single validator pass.
public boolean isValid(String password, ConstraintValidatorContext ctx) {
List<String> issues = new ArrayList<>();
if (password.length() < 8) issues.add("Too short");
if (!password.matches(".*\\d.*")) issues.add("Must contain a digit");
if (issues.isEmpty()) return true;
ctx.disableDefaultConstraintViolation();
issues.forEach(msg -> ctx.buildConstraintViolationWithTemplate(msg).addConstraintViolation());
return false;
}Spring-Managed Validators
Since UniqueEmailValidator is annotated with @Component, Spring injects repositories into it. This only works if the validator is bootstrapped through Spring's validator, which Spring Boot wires automatically.
Testing Custom Validators
Unit-test validators directly — no Spring context needed for pure validation logic. Use Validation.buildDefaultValidatorFactory() for integration tests.
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
CreateUserRequest req = new CreateUserRequest("", "bad-email", null);
Set<ConstraintViolation<CreateUserRequest>> violations = validator.validate(req);
System.out.println(violations.size()); // 3Quick Check
What interface does a custom constraint validator implement?
Recap
Create @interface with @Constraint(validatedBy=...). Implement ConstraintValidator. Use @Component for Spring injection. Class-level constraints validate multiple fields together. Compose existing constraints to avoid duplication.
Frequently asked questions
Is the “Custom Constraint Annotations” lesson free?
Yes — the full text of “Custom Constraint Annotations” is free to read here on the web, and the Java Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Java Academy course, upgrade to CoddyKit PRO.
What will I learn in “Custom Constraint Annotations”?
Create a custom @UniqueEmail annotation with a ConstraintValidator implementation. You practise Java Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Java Academy?
No prior experience is required. Java Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Custom Constraint Annotations” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Java Academy lesson?
Yes. Every Java Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.