CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-jakarta-validation--jakarta-validation-api

Jakarta Validation API defines a metadata model and API for JavaBean and method validation

Pending
Overview
Eval results
Files

custom-constraints.mddocs/

Custom Constraints

Framework for defining custom validation constraints through annotations and validator implementations with support for composed constraints and cascading validation.

Capabilities

Constraint Definition

Meta-annotation for marking annotations as Jakarta Validation constraints.

/**
 * Marks an annotation as being a Jakarta Validation constraint
 * Must be applied to constraint annotations
 */
@Target({ANNOTATION_TYPE})
@Retention(RUNTIME)
@interface Constraint {
    /**
     * ConstraintValidator classes must reference distinct target types
     * If target types overlap, a UnexpectedTypeException is raised
     * @return array of ConstraintValidator classes implementing validation logic
     */
    Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
}

Constraint Validator

Interface defining the validation logic for custom constraints.

/**
 * Defines the logic to validate a given constraint for a given object type
 * Implementations must be thread-safe
 * @param <A> the annotation type handled by this validator
 * @param <T> the target type supported by this validator
 */
interface ConstraintValidator<A extends Annotation, T> {
    /**
     * Initialize the validator in preparation for isValid calls
     * @param constraintAnnotation annotation instance for a given constraint declaration
     */
    default void initialize(A constraintAnnotation) {
        // Default implementation does nothing
    }
    
    /**
     * Implement the validation logic
     * @param value object to validate (can be null)
     * @param context context in which the constraint is evaluated
     * @return false if value does not pass the constraint
     */
    boolean isValid(T value, ConstraintValidatorContext context);
}

Constraint Validator Context

Context providing contextual data and operation when applying a constraint validator.

/**
 * Provides contextual data and operations when applying a ConstraintValidator
 */
interface ConstraintValidatorContext {
    /**
     * Disable the default constraint violation
     * Useful when building custom constraint violations
     */
    void disableDefaultConstraintViolation();
    
    /**
     * Get the default constraint message template
     * @return default message template
     */
    String getDefaultConstraintMessageTemplate();
    
    /**
     * Get the ClockProvider for time-based validations
     * @return ClockProvider instance
     */
    ClockProvider getClockProvider();
    
    /**
     * Build a constraint violation with custom message template
     * @param messageTemplate message template for the violation
     * @return ConstraintViolationBuilder for building custom violations
     */
    ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate);
    
    /**
     * Unwrap the context to a specific type
     * @param type target type to unwrap to
     * @return unwrapped instance
     * @throws ValidationException if unwrapping is not supported
     */
    <T> T unwrap(Class<T> type);
    
    /**
     * Builder for constraint violations with custom property paths and messages
     */
    interface ConstraintViolationBuilder {
        /**
         * Add a node to the path the constraint violation will be associated to
         * @param name property name
         * @return updated builder
         */
        NodeBuilderDefinedContext addPropertyNode(String name);
        
        /**
         * Add a bean node to the path
         * @return updated builder
         */
        NodeBuilderCustomizableContext addBeanNode();
        
        /**
         * Add a container element node to the path
         * @param name container element name
         * @param containerType container type
         * @param typeArgumentIndex type argument index
         * @return updated builder
         */
        ContainerElementNodeBuilderDefinedContext addContainerElementNode(
            String name, Class<?> containerType, Integer typeArgumentIndex);
        
        /**
         * Add the constraint violation built by this builder to the constraint violation list
         * @return context for additional violations
         */
        ConstraintValidatorContext addConstraintViolation();
        
        /**
         * Base interface for node builders
         */
        interface NodeBuilderDefinedContext {
            ConstraintViolationBuilder addConstraintViolation();
        }
        
        /**
         * Customizable node builder context
         */
        interface NodeBuilderCustomizableContext {
            NodeContextBuilder inIterable();
            ConstraintViolationBuilder addConstraintViolation();
        }
        
        /**
         * Context for building node details
         */
        interface NodeContextBuilder {
            NodeBuilderDefinedContext atKey(Object key);
            NodeBuilderDefinedContext atIndex(Integer index);
            ConstraintViolationBuilder addConstraintViolation();
        }
        
        /**
         * Container element node builder context
         */
        interface ContainerElementNodeBuilderDefinedContext {
            ContainerElementNodeContextBuilder inIterable();
            ConstraintViolationBuilder addConstraintViolation();
        }
        
        /**
         * Container element node context builder
         */
        interface ContainerElementNodeContextBuilder {
            ContainerElementNodeBuilderDefinedContext atKey(Object key);
            ContainerElementNodeBuilderDefinedContext atIndex(Integer index);
            ConstraintViolationBuilder addConstraintViolation();
        }
    }
}

Cascading Validation

Annotation for marking properties, method parameters, or return values for validation cascading.

/**
 * Marks a property, method parameter or method return type for validation cascading
 * Constraints defined on the object and its properties are validated
 */
@Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@interface Valid {}

Composed Constraints

Annotations for creating composed constraints from multiple constraints.

/**
 * Marks a composed constraint as returning a single constraint violation report
 * All constraint violations from composing constraints are ignored
 */
@Target({ANNOTATION_TYPE})
@Retention(RUNTIME)
@interface ReportAsSingleViolation {}

/**
 * Marks a constraint attribute as overriding another constraint's attribute  
 * Used in composed constraints to override composing constraint attributes
 */
@Target({METHOD})
@Retention(RUNTIME)
@interface OverridesAttribute {
    /**
     * The constraint whose attribute this element overrides
     * @return constraint class
     */
    Class<? extends Annotation> constraint();
    
    /**
     * Name of the attribute to override
     * @return attribute name
     */
    String name();
}

Usage Examples:

import jakarta.validation.*;
import jakarta.validation.constraints.*;

// 1. Simple custom constraint
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {PositiveEvenValidator.class})
public @interface PositiveEven {
    String message() default "Must be a positive even number";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// Validator implementation
public class PositiveEvenValidator implements ConstraintValidator<PositiveEven, Integer> {
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        if (value == null) {
            return true; // Let @NotNull handle null validation
        }
        return value > 0 && value % 2 == 0;
    }
}

// 2. Custom constraint with custom violation messages
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {PasswordValidator.class})
public @interface ValidPassword {
    String message() default "Invalid password";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    
    boolean requireUppercase() default true;
    boolean requireDigits() default true;
    int minLength() default 8;
}

public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
    private boolean requireUppercase;
    private boolean requireDigits;
    private int minLength;
    
    @Override
    public void initialize(ValidPassword annotation) {
        this.requireUppercase = annotation.requireUppercase();
        this.requireDigits = annotation.requireDigits();
        this.minLength = annotation.minLength();
    }
    
    @Override
    public boolean isValid(String password, ConstraintValidatorContext context) {
        if (password == null || password.length() < minLength) {
            return false;
        }
        
        context.disableDefaultConstraintViolation();
        boolean isValid = true;
        
        if (requireUppercase && !password.matches(".*[A-Z].*")) {
            context.buildConstraintViolationWithTemplate("Password must contain uppercase letter")
                   .addConstraintViolation();
            isValid = false;
        }
        
        if (requireDigits && !password.matches(".*\\d.*")) {
            context.buildConstraintViolationWithTemplate("Password must contain digit")
                   .addConstraintViolation();
            isValid = false;
        }
        
        return isValid;
    }
}

// 3. Composed constraint
@NotNull
@Size(min = 2, max = 50)
@Pattern(regexp = "^[A-Za-z ]+$")
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@ReportAsSingleViolation
public @interface ValidName {
    String message() default "Invalid name format";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 4. Cross-field validation
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {PasswordMatchesValidator.class})
public @interface PasswordMatches {
    String message() default "Passwords don't match";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, Object> {
    @Override
    public boolean isValid(Object obj, ConstraintValidatorContext context) {
        // Assume obj has getPassword() and getConfirmPassword() methods
        try {
            String password = (String) obj.getClass().getMethod("getPassword").invoke(obj);
            String confirmPassword = (String) obj.getClass().getMethod("getConfirmPassword").invoke(obj);
            
            boolean matches = Objects.equals(password, confirmPassword);
            
            if (!matches) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate("Passwords don't match")
                       .addPropertyNode("confirmPassword")
                       .addConstraintViolation();
            }
            
            return matches;
        } catch (Exception e) {
            return false;
        }
    }
}

// Usage in a class
@PasswordMatches
public class UserRegistration {
    @ValidName
    private String firstName;
    
    @ValidPassword(minLength = 10, requireUppercase = true, requireDigits = true)
    private String password;
    
    private String confirmPassword;
    
    @PositiveEven
    private Integer luckyNumber;
    
    // getters and setters...
}

Install with Tessl CLI

npx tessl i tessl/maven-jakarta-validation--jakarta-validation-api

docs

bean-validation.md

bootstrap-configuration.md

constraints.md

container-validation.md

custom-constraints.md

index.md

metadata.md

method-validation.md

validation-groups.md

tile.json