Search code examples
javaspringvalidationspring-data-jpathymeleaf

Display error validated by method in Thymeleaf


I am using Spring Data JPA to model and validate my data. In this case I got a class that got both a password and confirm field:

public class RegistrationForm {

    private String password;

    private String confirm;

    // ...
}

and now I want to check if they both match. I figured out I can create a method for it using @AssertTrue:

@AssertTrue(message = "Passwords don't match")
private boolean isPasswordMatch() {
    return password.equals(confirm);
}

Now in my controller I validate this class and this method runs just fine. My problem now is that I couldn't figure out how to display this error in my Thymeleaf template. I usually used this for fields with validation:

<span th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></span>

But that doesn't work with methods. Now I investigated a bit and found out that if I name the method something like isXXX then it is going to put a field into the Errors instance named XXX. In this case, it would be a field called passwordMatch. I could verify this using a debugger.

debugging

It doesn't work, even though that field error exists as a ViolationFieldError like any other error. For reference I tried this:

<span th:if="${#fields.hasErrors('passwordMatch')}" th:errors="*{passwordMatch}"></span>

I simply get an error saying that the property is not readable:

Caused by: org.springframework.beans.NotReadablePropertyException: Invalid property 'passwordMatch' of bean class [me.squidxtv.tacocloud.model.RegistrationForm]: 
Bean property 'passwordMatch' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?

Note: I am currently going through "Spring in Action" 6th edition and this comes up in chapter 5 "Securing Spring" but my question isn't related to it, because the book itself doesn't implement validation for that class.


Solution

  • If you use #fields.hasErrors('passwordMatch'), it expects a property passwordMatch so one way would be making the isPasswordMatch() method public. This is probably the easiest way to do this. When doing that, you might want to change password.equals(confirm) to Objects.equals(password, confirm).

    However, Thymeleaf also allows you to access all errors without checking the fields by accessing #fields.errors() or #fields.detailedErrors().

    If you want to iterate over all errors, you can do so using #fields.errors():

    <div th:if="${#fields.hasErrors()}" th:each="err : *{#fields.errors()}">
        <!-- Tell the user about the error -->
        <span th:text=${err}></span>
    </div>
    

    However, that doesn't let you filter for specific errors. If you want to use the information that the error occured in passwordMatch, you need to use detailedErrors(). You can iterate over that as follows:

    <div th:if="${#fields.hasErrors()}" th:each="err : *{#fields.detailedErrors()}">
        Error in <span th:text=${err.fieldName}></span>: <span th:text=${err.message}></span>
    </div>
    

    With that information, you can also filter for passwordMatch:

    <th:block th:if="${#fields.hasErrors()}" th:each="err : *{#fields.detailedErrors()}">
        <div th:if="${'passwordMatch'.equals(err.fieldName)}" th:text=${err.message}></div>
    </th:block>