0Pricing
Java Academy · Lesson

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()); // 3

Quick 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.

All lessons in this course

  1. Bean Validation: @NotNull, @Size, @Pattern
  2. Custom Constraint Annotations
  3. Global Exception Handling with @ControllerAdvice
  4. RFC 7807 Problem Details and Consistent Error Responses
← Back to Java Academy