Search code examples
javaspringvalidationspring-mvcthymeleaf

Spring + Thymeleaf custom validation display


I've been trying to get custom javax validation working (Spring Boot & Thymeleaf), but I cannot figure out how to display the error message. The problem seems to be, that "normal" errors (e.g. @Size, @NotNull, etc.) seem to add a a FieldError to the binding result. My custom validator delivers an ObjectError though. I can't figure out how to get Thymeleaf to display my custom error (which obviously gets passed, since th:errors="*{*}" shows it).

Any help is greatly appreciated.

UPDATE: I can now display the error message via

<p th:if="${#fields.hasErrors('${user}')}" th:errors="${user}"></p>

However, if I need more than one validator (e.g. confirm password and confirm email) this solution will not work (or display both error messages if one doesn't fit. Don't hesitate if you have a suggestion.

Below is the code I've used for this:

Template:

<p th:if="${#fields.hasErrors('username')}"th:errors="*{username}"></p>
<!-- works just fine -->
<p th:if="${#fields.hasErrors('*')}" th:errors="*{*}"></p>
<!-- works and displays all errors (for every field with an error,
 including my custom validator) -->
<p th:if="${#fields.hasErrors('confirmPassword')}" th:errors="*{*}"></p>
<!-- does not work -->
<p th:if="${#fields.hasErrors('*')}" th:errors="*{confirmPassword}"></p>
<!-- does not work -->

Validator implementation:

public class PasswordsEqualConstraintValidator implements
        ConstraintValidator<PasswordsEqualConstraint, Object> {

    @Override
    public void initialize(PasswordsEqualConstraint arg0) {
    }

    @Override
    public boolean isValid(Object candidate, ConstraintValidatorContext arg1) {
        User user = (User) candidate;
        return user.getPassword().equals(user.getConfirmPassword());
    }
}

Validator interface:

@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = PasswordsEqualConstraintValidator.class)
public @interface PasswordsEqualConstraint {
    String message();

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

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

User.java:

@PasswordsEqualConstraint(message = "passwords are not equal")
public class User implements java.io.Serializable {
...     
@Size(min=2, max=40)
private String username;
...
private String confirmPassword;
...

Controller:

@RequestMapping(value = "/signup", method = RequestMethod.POST)
public String signup(@Valid User user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "signup";
        }
        ... do db stuff .. return "success";
}

Solution

  • This is probably because your @PasswordsEqualConstraint is assigned to the whole bean (type) not field "confirmPassword". To add possible constraint violation to a concrete field you may do like on example below.

    FieldMatch compares two fields if they are not equal then the validation error is assigned to the second field.

    BTW. this is more generic solution for the thing you are doing. Fo example for passwords you may use it like

    @FieldMatch(first = "password", second = "confirmPassword", message = "Passowords are not equal.")
    

    validator:

    public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {
    
      private String firstFieldName;
      private String secondFieldName;
    
      @Override
      public void initialize(final FieldMatch constraintAnnotation) {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
      }
    
      @Override
      public boolean isValid(final Object value, final ConstraintValidatorContext context) {
        try {
          final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
          final Object secondObj = BeanUtils.getProperty(value, secondFieldName);
    
          boolean isValid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
    
          if (!isValid) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation();
          }
    
          return isValid;
        }
        catch (final Exception ignore) {
          // ignore
        }
        return true;
      }
    }