Streamlining Data Integrity
Traditional imperative validation often relies on verbose conditional blocks that clutter business logic. Consider this manual approach:
public class ManualCheck {
public static String validate(String identifier, Integer count) {
if (identifier == null || identifier.isBlank()) {
return "Identifier cannot be empty.";
}
if (count == null || count <= 0) {
return "Count must be a positive integer.";
}
return null;
}
}
As requirements grow, this procedural pattern leads to unmaintainable nesting. The Bean Validation (JSR 380) specification provides a declarative, annotation-driven alternative to decouple validation logic from core business code. This approach leverages metadata to define constraints directly on class members.
Standard Validation Workflow
While the Java API defines the interfaces, implementations like Hibernate Validator provide the runtime engines necessary for these features. To use them, define your domain object with constraint annotations:
public class Product {
@NotNull
@Size(min = 1, max = 50)
private String sku;
@Min(0)
@Max(100)
private int stockLevel;
// Getters and setters omitted
}
Executing validation involves the Validator engine:
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Product item = new Product();
Set<ConstraintViolation<Product>> violations = validator.validate(item);
for (ConstraintViolation<Product> issue : violations) {
System.out.println(issue.getPropertyPath() + ": " + issue.getMessage());
}
Custom Constraint Development
When standard annotations are insufficient, you can create custom constraints consisting of an annotation interface and a backing validator implementation.
Composing Annotations
You can create meta-annotations to bundle existing constraints:
@Min(10)
@Max(500)
@Constraint(validatedBy = {})
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPriceRange {
String message() default "Price out of bounds";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Custom Validator Logic
For complex logic, implement the ConstraintValidator interface. First, define the annotation:
@Constraint(validatedBy = { CategoryValidator.class })
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidCategory {
String message() default "Invalid category provided";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Then, implement the validation logic:
public class CategoryValidator implements ConstraintValidator<ValidCategory, String> {
private static final List<String> VALID_TYPES = List.of("ELECTRONICS", "BOOKS", "HOME");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value != null && VALID_TYPES.contains(value.toUpperCase());
}
}
Advanced Scenarios: Groups and Payloads
Validation groups allow you to apply specific constraints under different business contexts, such as distinguishing between "Draft" and "Published" states. Simply define an empty interface to represent the group and apply it to the constraint:
public interface ExtendedCheck {}
public class Product {
@Min(value = 0, groups = ExtendedCheck.class)
private int extendedStatus;
}
// Trigger validation for specific groups
validator.validate(product, ExtendedCheck.class);
Furthermore, the payload attribute enables categorizing violations (e.g., distinguishing between errors and warninsg), which can be retrieved during the violation analysis to dictate UI behavior or logging severity.