[Feature Contribution Request] @Compare constraint

Hi everyone,

I’d like to contribute a small new constraint to Hibernate Validator and wanted to get feedback before starting implementation.

The idea is a minimal @Compare class-level constraint for common cross-field validation cases such as:

  • startDate < endDate

  • min <= max

  • password == confirmPassword

Example:

@Compare(
    left = "startDate",
    right = "endDate",
    operator = Compare.Operator.LT
)

The MVP scope would intentionally stay very small:

  • class-level only,

  • two property paths,

  • comparison operators,

  • only naturally comparable values,

  • validation skipped if either side is null,

  • no expression language, parsing, coercion, or custom comparators.

Would maintainers be open to something like this?

If the direction makes sense, I’d be happy to work on an implementation and tests.

IMO, if we add property names as strings, we could just as well use an EL constraint e.g. @ExpressionValid("startDate < endDate"), but that’s just my 2 cents.

@mbekhta may want to chime in :slight_smile:

hey :waving_hand:

thanks for reaching out! In general, yes, we are happy to include reusable constraints in Hibernate Validator.

About this specific constraint:

  • if we want to give a constraint access to more than one attribute, it has to be a class-level constraint. Just wanted to clarify this, as your suggestion for the MVP can be interpreted that in the next iterations, we could make it work on a field or getter :slightly_smiling_face:
  • we stay away from using reflection inside the constraints’ isValid calls. If we provide just two string paths, it means we will need to look up the fields through reflection on each isValid call (we cannot be sure what exact type we are getting as an arg to the isValid). Have you considered having an extractor as part of the constraint? Something like:
interface @Compare {
   Operator operator();
   Class<OperandsExtractor> operands();
   // other usual atttributes: message/groups/payload ...
}

interface OperandsExtractor<T> {
   T left();
   T right();
   default boolean extractAndCheck(T instance, Operator operator) {
      ...
   }
}

and then having something like:
@Compare(
    left = "startDate",
    right = "endDate",
    operands = MySpecificTypeWithStartEndDateOperandsExtractor.class,
    operator = Compare.Operator.LT
)