Vaadin REST validation response

Viewed 54

When a REST endpoint returns an validation error I want to reflect the validation message in the corresponding vaadin input field. In this example I would like to have an error message near to the password input field. So the user is able to fix the error easily.

@Route("create-customer") 
public class CreateCustomerView extends Div {

    public CreateCustomerView(RestClientService restClientService) {
        TextField firstName = new TextField("First name");
        DatePicker birthDay = new DatePicker("Birthday");
        PasswordField password = new PasswordField("Password");
        Button submit = new Button("Submit", (ComponentEventListener<ClickEvent<Button>>) event -> restClientService.createCustomer(
                new RegisterCustomerRequest(firstName.getValue(), birthDay.getValue(), password.getValue())));

        FormLayout formLayout = new FormLayout();
        formLayout.add(firstName, password, submit);

        add(formLayout);
    }
}

My repsonse is very standard and verbose.

{
    "timestamp": "2022-09-17T12:39:55.642883700",
    "status": 400,
    "error": "ConstraintViolationException",
    "message": "Some arguments are not valid.",
    "fieldErrors": [
        {
            "field": "register.registerCustomerCommand.password",
            "error": "com.example.application.service.RegisterCustomerService register.registerCustomerCommand.password: # a digit must occur at least once\n# a lower case letter must etc. etc."
        }
    ],
    "path": "/v1/customers/"
}

Is it possible with vaadin out of the box? Binder seems to be good for client side validation errors but my errors depend on the server side validation on the request.

2 Answers

As long as you can get the validation answer quickly enough, you can just use Binder since it doesn't care about the origin of the validation result. If the response is slow, then you might have to validate asynchronously, but then you're on your own without direct help from Binder.

Solved it like this:

  • introduced ServersideValidationException that is thrown if response from server contains the error "ConstraintViolationException". In this case I deserialize the content to an Error that contains List of fieldErrors with path and errorMessage and pass it to the Exception.
  • map these fieldErrors trough the binder to the errorMessage of a Component.
    // button action
    private void validateAndSave() {
        if (binder.isValid()) {
            try {
                binder.writeBean(registerCustomerRequest);
                final CustomerResponse customer = restClientService.createCustomer(registerCustomerRequest);
            } catch (ValidationException e) {
                throw new ApplicationException(e);
            } catch (ServersideValidationException e) {
                populateErrorsToForm(e);
            }
        }
    }

    private void populateErrorsToForm(ServersideValidationException e) {
        e.getFieldErrors().forEach(fieldError -> {
            final String fieldName = StringUtils.substringAfterLast(fieldError.getPath(), ".");
            final var binding = binder.getBinding(fieldName).orElseThrow(fieldNotFoundInForm(fieldName));
            BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(binding.getField());
            wrapper.setAutoGrowNestedPaths(true);
            wrapper.setPropertyValue("invalid", true);
            wrapper.setPropertyValue("errorMessage", fieldError.getErrorMessage());
        });
    }

Sidenote: Binder is not required for setting the error but it provides handy method binder.getBinding(fieldName). Does not support nested errors yet it is feasible. It is ugly.

Related