Spring Boot: From Basics to Production · Validation and Error Handling

Custom Constraints and Cross-Field Validation

Write reusable validation annotations and class-level checks.

When built-in constraints aren't enough, create a custom constraint: an annotation plus a ConstraintValidator. Class-level constraints handle cross-field rules (end date after start date, matching passwords). Keep validators fast and side-effect free; validators are Spring beans, so they can receive dependencies, but avoid heavy I/O.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateRangeValidator.class)
public @interface ValidDateRange {
    String message() default "endDate must be after startDate";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class DateRangeValidator implements ConstraintValidator<ValidDateRange, BookingRequest> {
    @Override
    public boolean isValid(BookingRequest value, ConstraintValidatorContext ctx) {
        if (value == null || value.startDate() == null || value.endDate() == null) return true;   // let @NotNull handle nulls
        return value.endDate().isAfter(value.startDate());
    }
}

@ValidDateRange
public record BookingRequest(@NotNull LocalDate startDate, @NotNull LocalDate endDate, @Min(1) int guests) {}

Common pitfalls

  • Validators that return false for null (duplicating @NotNull and producing double errors).
  • Database calls in validators for every request.
  • Generic messages that don't tell users which field is wrong.

Key points

  • Annotation + ConstraintValidator
  • Class-level for cross-field rules
  • Treat null as valid; combine with @NotNull

Further reading: Jakarta Bean Validation spec

Practice questions

Why should custom validators usually treat null as valid?

Answer

So null checks stay the job of @NotNull; otherwise you get duplicate error messages and can't make a field optional.

How do you validate that two fields relate correctly?

Answer

Use a class-level constraint whose validator receives the whole object and compares the fields.

Open in the app to mark lessons complete, take notes and track your progress.