Jakarta Validation API defines a metadata model and API for JavaBean and method validation
—
Framework for defining custom validation constraints through annotations and validator implementations with support for composed constraints and cascading validation.
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();
}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);
}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();
}
}
}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 {}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