Spring Boot Thymeleaf Form Data Validation with Bean Validator

Introduction

Form Data Validation is a very common, and rudimentary step in building any web application with user input. We want to make sure that certain ranges are respected, and that certain formats are followed. For example, we'll want to make sure that the user isn't -345 years old, or that their email address is valid.

There are many ways to validate form data - and the method you use depends on your application. In general, you'll want to perform client-side validation, as well as server-side validation. Client-side validation makes sure that unfiltered data doesn't even reach the back-end, while server-side validation makes sure that the wrong data isn't processed further.

In this article, we'll cover how to perform form data validation in Spring Boot with Thymeleaf, as the template engine.

We'll leverage Spring Boot's built-in Bean Validation API, which makes this process simple and straightforward.

Spring Boot Validation Maven Dependency

Spring Boot allows us to define validation criteria using annotations. In your Domain Model, you can simply annotate fields with the constraints, and it'll enforce them.

For the validation annotations to work we need to add the following dependency:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Domain Model

Let's define a simple POJO, with a few fields that we want to validate:

@Entity
public class Person {

  @Id
  @GeneratedValue(strategy= GenerationType.AUTO)
  private Long id;

  @NotEmpty
  @Size(min = 5)
  private String fullName;

  @NotEmpty
  @Email
  private String email;

  @NotNull
  @Min(value = 18)
  private Integer age;

  // Getters and Setters

}

Now, let's break the annotations we used:

  • @NotEmpty - is used to constrain a field of type String, Collection, Map, or Array to not be null or empty.
  • @Size([min = x, max = y]) - is used to define the rules for the size of a String, Collection, Map, or Array.
  • @Email - helps us to validate the string against a regex which defines the structure of a valid email.
  • @NotNull - tells Spring the field should not be null, but it can be empty.
  • @Min and @Max are used to specify the limits of a variable. For example, the @Min age could be set to, say, 18.

Persistence Layer - Repository

To create a simple CRUD repository, all we have to do is extend the JpaRepository and supply our domain model and the ID's data type:

@Repository
public interface PersonRepository extends JpaRepository<Person, Long> {
}

Creating a Form with Thymeleaf

Now, let's make a simple form using HTML and Bootstrap to collect information:

<form th:action="@{/add}" th:object="${person}" method="post" class="form">
    <div class="form-group">
        <label for="fullName">Name</label>
        <input class="form-control" type="text" th:field="*{fullName}" id="fullName" placeholder="Full Name">
        <div class="alert alert-warning" th:if="${#fields.hasErrors('fullName')}" th:errors="*{fullName}"></div>
    </div>
    <div class="form-group">
        <label for="email">Email</label>
        <input class="form-control" type="text" th:field="*{email}" id="email" placeholder="Email">
        <div class="alert alert-warning" th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></div>
    </div>
    <div class="form-group">
        <label for="age">Age</label>
        <input class="form-control" type="text" th:field="*{age}" id="age" placeholder="Age">
        <div class="alert alert-warning" th:if="${#fields.hasErrors('age')}" th:errors="*{age}"></div>
    </div>
    <input type="submit"  class="btn btn-success" value="Add User">
</form>

The form points to /add and sends a POST request. The fields in our object, fullName, age and email are in the form, signified by th:field. Since we've got our th:object=${person},we can refer to this person object by substituting it with a * before the fields.

*{fullName} is the same as ${person.fullName}. Each input also has a hidden <div> which is shown only if the ${#fields.hasErrors()} calls evaluate to true. If there aren't errors, this div doesn't exist. If there are, the th:errors tag lets us specify a message. If we simply pass in the field that causes the error, such as th:errors="*{age}", the default message from the Bean Validator API are used, for that field.

This results in a form that looks like this:

basic html form

Since these messages aren't very user-friendly, we'll want to customize them by supplying our own messages.

Controller

Now, let's make a controller that'll handle a request to save a Person into the database. As usual, we'll have a @GetMapping() to show the form, and a @PostMapping to handle the request. In the method signature of the @PostMapping, we'll annotate the POJO with @Valid.

The @Valid annotation triggers the Bean Validator to check if the fields populated in the object conform to the annotations we've used in the class definition. If you don't use the @Valid annotation, it won't check anything, and even values you might not expect can be populated in the object.

In our case, the Person person object is the object that's populated with the inputs of a form:

@GetMapping("/add")
public String showAddPersonForm(Person person) {
  return "add-person";
}

@PostMapping("/add")
public String addPerson(@Valid Person person, BindingResult result, Model model) {
  if (result.hasErrors()) {
    return "add-person";
  }
  repository.save(person);
  return "redirect:/index";
}

If there are any issues with this Person, the ${fields} attribute will have errors within it, tied to the field that caused the error.

Customizing Error Messages

To set a custom message for any validation constraint, you can use the message option:

@NotEmpty(message = "Field can't be empty!)
private String field;

You can simply write these messages out in your model, like this. Though, it's considered good practice to put the validation messages in a properties file, that you can reference. This is done since you can bundle all validation messages together, and can update them if the models get changed at a later date.

Let's make our ValidationMessages.properties under src/main/resources:

Size.Person.FullName=The Full Name should have at least 5 characters

We will then modify the Person model, and supply this property as the message of the @Size annotation:

@NotEmpty(message = "The Full Name can't be null")
@Size(min = 5, message = "{Size.Person.FullName}")
private String fullName;

Let's go back to our form and see how it looks now:

customized error messages spring boot bean validator

Conclusion

In this article, we've gone over how to use the Bean Validator API that Spring Boot uses to effortlessly perform form data validation with Thymeleaf.

We've created a domain model, annotated our fields with Bean Validator constraints. Then, we've made a repository and controller to display the form and process it, as well as save the relevant information into a Person object, making sure that the fields are validated with the @Valid annotation.

You can find the source code on GitHub.