Building a Spring-Integrated Custom Annotation for Bean Validation

Defining the Custom Constraint Annotation

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Constraint(validatedBy = RecordIdValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RecordExists {

    String message() default "The provided identifier does not exist";

    boolean mandatory() default false;

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

Base Validator with Service Injection

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.annotation.Annotation;
import java.util.Objects;
import java.util.function.Predicate;

@Slf4j
public abstract class SpringAwareValidator<A extends Annotation, T>
        implements ConstraintValidator<A, T> {

    protected boolean mandatoryCheck;

    protected Predicate<T> validationRule = input -> true;

    @Autowired
    protected PersistenceService persistenceService;

    @Override
    public boolean isValid(T value, ConstraintValidatorContext context) {
        if (!mandatoryCheck && Objects.isNull(value)) {
            return true;
        }
        return validationRule.test(value);
    }
}

Concrtee Implementation for Identifier Lookup

public class RecordIdValidator extends SpringAwareValidator<RecordExists, String> {

    @Override
    public void initialize(RecordExists annotation) {
        validationRule = id -> {
            long matches = persistenceService.count(
                    new QueryWrapper<DataRecord>()
                            .eq("recordId", id)
            );
            return matches > 0;
        };
        this.mandatoryCheck = annotation.mandatory();
        super.initialize(annotation);
    }
}

Applying the Annotation and Group Sequences

Inside a transefr object:

public class UpdateRequest {

    @ApiModelProperty("Record identifier")
    @NotEmpty(message = "Identifier must not be blank")
    @RecordExists(groups = ValidationLevel.Critical.class)
    @Length(max = 32, message = "Identifier length cannot exceed 32 characters")
    private String recordId;

    public interface Critical {}
    public interface Extended {}

    @GroupSequence({Default.class, Critical.class, Extended.class})
    public interface OrderedChecks {}
}

Tirggering Validation in a REST Controller

@PostMapping
public ResponseEntity<?> update(@RequestBody @Validated(UpdateRequest.OrderedChecks.class) UpdateRequest payload) {
    // process valid request
}

Tags: bean-validation Spring custom-validator javax-validation

Posted on Fri, 08 May 2026 17:54:33 +0000 by cmanhatton