flow icon indicating copy to clipboard operation
flow copied to clipboard

Better support for bean validation on class level

Open obfischer opened this issue 1 year ago • 10 comments

Describe your motivation

Some of my domain objects have constraints between different fields. For example if value a > 10, value b must be given etc. etc. To express such constraints with the help of Java Bean Validation is possible. For such cases, I can to write custom constraints on class level.

Unfortunately I don't know a clean way to relate such failing constraints to the fields in my forms.

Describe the solution you'd like

I need a way to relate a given constraint violation to one or more fields in my forms, so that I can highlight it and the user which fields are not valid.

Describe alternatives you've considered

Currently I add validators via Binder#withValidator() to the binder before I all Binder#bindInstanceFields().

Additional context

It would be great to have only one approach for validation, which can be used for the frontend as well as for the backend.

obfischer avatar Jul 20 '22 19:07 obfischer

@obfischer thank you for creating an issue!

Currently I add validators via Binder#withValidator() to the binder before I all Binder#bindInstanceFields()

could you please give a code example to us showing how you're doing it currently, just for making it clear for us how could an API for cross-field validation be looks like.

mshabarov avatar Sep 16 '22 11:09 mshabarov

Any idea when this ticket will eventually be picked up? I mean, bean validation at class level is something very common... I guess almost everybody using JSR-303 bean validations uses class level annotations.

sandronm avatar Oct 17 '22 09:10 sandronm

@sandronm We have no plans to pick it in the nearest future, as well as no plans to include it into V23.3. Vaadin 24 would be a good candidate to have this feature.

mshabarov avatar Oct 17 '22 09:10 mshabarov

What would this feature look like in practice?

If the bean has two separate validators related to different fields, then what would you expect to write in application code to help the binder understand where each validation error should be shown?

Legioth avatar Nov 02 '22 20:11 Legioth

What would this feature look like in practice?

If the bean has two separate validators related to different fields, then what would you expect to write in application code to help the binder understand where each validation error should be shown?

On top of your form. It will be indeed difficult to directly mark as invalid any input (textfield, checkbox, ...).

sandronm avatar Nov 02 '22 21:11 sandronm

Everything exists to make this work other than catching the class level constraint violation bit. Here's what I have:

	entityBinder.withValidator(new ClassValidator());
	entityBinder.setStatusLabel(errorText);

...

private static class ClassValidator extends AbstractValidator<Attribute> {

	protected ClassValidator() {
		super("Class level validator(s) failed");
	}

	@Override
	public ValidationResult apply(Attribute value, ValueContext context) {
		var violations = validator.validate(value);
		for (var violation : violations) {
			var path = violation.getPropertyPath();
			/*
			 * Property level violations will be handled by the binder
			 */
			if (path != null && !path.toString().isBlank()) {
				continue;
			}

			/*
			 * There's (probably) only one class level constraint, so just
			 * return the first found.
			 */
			return ValidationResult.error(violation.getMessage());
		}

		return ValidationResult.ok();
	}
}

Traivor avatar Nov 02 '22 21:11 Traivor

I was asking specifically for the case from the original ticket description:

I need a way to relate a given constraint violation to one or more fields in my forms, so that I can highlight it and the user which fields are not valid.

The need to define a custom class like the ClassValidator example is also a known limitation, but that's a slightly different issue and one for which a workaround does exist.

My question was about ideas for how to design an API that allows that different class-level constraint messages should be shown as error labels for different form fields.

As an extreme example, imagine that we've got a form with two text fields, textField1 and textField2 and a bean with three different class-level constraints:

@OneClassConstraint
@RepeatedClassConstraint("foo")
@RepeatedClassConstraint("bar")
public class MyBean {
 // Some fields, setters and getters here as well
}

If we want errors from @OneClassConstratint to be shown for textField1, errors from @RepeatedClassConstraint("foo") shown for textField2 and @RepeatedClassConstraint("bar") in a binder level status label (i.e. Binder::setStatusLabel), then what could the optimal application code look like to configure the binder in that way if we have full freedom to make changes to the API of BeanValidationBinder?

Legioth avatar Nov 03 '22 08:11 Legioth

I have the same issue.

The basic problem is this: there are many examples of field-level validation logic where the determination of "valid" depends on other fields as well as the field being validated.

A simple example is a "Confirm password" field, where you have a "Password" field and a "Confirm password" field. The "Confirm password" field has the following field-level validation constraint: This field's value must be the same as the "Password" field's value.

While this validation constraint does involve multiple fields, it is intuitively "field-level" because when the constraint is violated, there is only one field that is "wrong" (namely, the "Confirm password" field) and moreover you want the validation error to appear next to that field.

There is a simple & easy way we could accomplish this with minimal change to the current validation API: Add a new method ValueContext.getBinder() which returns the Binder associated with the validation operation being performed.

Then the "Confirm password" problem is easily solved like this:

public class ConfirmPasswordValidator implements Validator<String> {

    @Override
    public ValidationResult apply(String confirmation, ValueContext context) {
        PasswordField passwordField = context.getBinder().getBinding("password").get().getField();
        if (!Objects.equals(confirmation, passwordField.getValue()))
            return ValidationResult.error("Passwords must match"));
        return ValidationResult.ok();
    }
}

FYI, added this as a separate feature request #19060

archiecobbs avatar Mar 27 '24 20:03 archiecobbs

Note that the ConfirmPasswordValidator in itself is not enough for the use case of a field that is validated based on the value of some other field. In addition, you also need to invalidate the validation status for the confirm field when the value of the password field changes.

Legioth avatar Mar 28 '24 07:03 Legioth

Good point - though that's not an issue if, as in my case, you call binder.setFieldsValidationStatusChangeListenerEnabled(false) to defer validation until the form is submitted.

In fact, the two kind of go hand-in-hand.

archiecobbs avatar Mar 28 '24 13:03 archiecobbs