Careeroza
Custom Validator
When Built-in Constraints Are Not Enough
ShopEase product codes (SKUs) look like ELE-1234: three capital letters for the category, a dash and four digits. Write the rule once as a custom constraint and reuse it everywhere. First test the logic in plain Java:
Example
import java.util.List;
public class Main {
static boolean isValidSku(String sku) {
if (sku == null) return true; // like built-in constraints: use @NotNull for required
if (sku.length() != 8 || sku.charAt(3) != '-') return false;
for (int i = 0; i < 3; i++) if (!Character.isUpperCase(sku.charAt(i))) return false;
for (int i = 4; i < 8; i++) if (!Character.isDigit(sku.charAt(i))) return false;
return true;
}
public static void main(String[] args) {
List<String> inputs = List.of("ELE-1234", "BOO-0042", "ele-1234", "ELE1234", "EL-12345", "ELE-12A4");
for (String s : inputs) System.out.printf("%-9s -> %s%n", s, isValidSku(s) ? "valid" : "invalid");
System.out.println("null -> " + (isValidSku(null) ? "valid" : "invalid"));
}
}Output
ELE-1234 -> valid BOO-0042 -> valid ele-1234 -> invalid ELE1234 -> invalid EL-12345 -> invalid ELE-12A4 -> invalid null -> valid
The Annotation and the Validator
import jakarta.validation.*;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = SkuValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.RECORD_COMPONENT})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidSku {
String message() default "must look like ABC-1234";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SkuValidator implements ConstraintValidator<ValidSku, String> {
@Override
public boolean isValid(String sku, ConstraintValidatorContext context) {
return sku == null || sku.matches("[A-Z]{3}-[0-9]{4}");
}
}
// Use it like any built-in constraint
public record CreateProductRequest(@NotNull @ValidSku String sku, @NotBlank String name, ...) {}Tip: A ConstraintValidator is created by Spring, so you can inject beans into it (e.g. a repository to check that a category exists).
Class-Level Constraint
Some rules involve two fields, e.g. "sale price must be lower than price". Put the constraint on the class (record) and check the whole object:
@Constraint(validatedBy = SalePriceValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SalePriceBelowPrice {
String message() default "salePrice must be lower than price";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class SalePriceValidator implements ConstraintValidator<SalePriceBelowPrice, PriceRequest> {
public boolean isValid(PriceRequest r, ConstraintValidatorContext ctx) {
return r.salePrice() == null || r.price() == null || r.salePrice().compareTo(r.price()) < 0;
}
}
@SalePriceBelowPrice
public record PriceRequest(@NotNull BigDecimal price, BigDecimal salePrice) {}Exercise: Write a @IndianPincode constraint: exactly 6 digits, first digit not 0.
Show answer
public class PincodeValidator implements ConstraintValidator<IndianPincode, String> {
public boolean isValid(String pin, ConstraintValidatorContext ctx) {
return pin == null || pin.matches("[1-9][0-9]{5}");
}
}