Spring Boot – Validación con Spring MVC y thymeleaf

La validación de los datos que un usuario puede introducir en una aplicación es algo completamente necesario para garantizar que todo funcione como se espera, aunque siempre tenemos la opción de hacer la validación en el navegador con javaScript también deberíamos de hacer una validación en el servidor porque no podemos controlar que el usuario no desactive la ejecución de javaScript en su navegador y por lo tanto se salte nuestras validaciones.

La validación en spring es realmente sencilla y si no necesitamos crear valores personalizados lo único que necesitamos es añadir anotaciones a las propiedades para indicar que condiciones queremos que se cumplan.

Validar un objeto con Spring

Vamos a verlo con un ejemplo, y para no empezar desde 0 vamos a seguir reutilizando el proyecto con el que hemos creando nuestra aplicación con Spring Boot.


package com.programandoointentandolo.tsb.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

@Entity
public class Producto {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @Size(min = 3, max = 8, message="El código del producto tiene que tener entre 3 y 8 caracteres")
    @Pattern(regexp = "[A-Z0-9]+", message="El código del producto solo puede tener letras mayúsculas o números")
    private String codigo;

    @NotEmpty(message="El nombre del producto es obligatorio")
    private String nombre;

    @NotNull(message="El precio es obligatorio")
    @Min(2, message="El precio mínimo es 2")
    @Max(100, message="El precio máximo es 100")
    private Double precio;

    public Producto() {

    }

    // Getters y setters 

}

En este ejemplo hemos anotado las propiedades de nuestra clase producto para que cada campo tenga alguna restricción, como se puede ver una propiedad puede tener varios validadores.

Aunque como ejemplo he incluido el mensaje que se mostrará si no se cumplen las condiciones en la anotación en la práctica lo normal es que tengamos estos mensajes internacionalizados y por lo tanto los eliminaremos de la anotación y los añadiremos en nuestros messages.properties. Si no se añade un mensaje personalizado se mostrará el mensaje de error predefinido de cada validador que también puede ser suficiente.

Si queremos añadir los mensajes de error en los messages.properties las keys tienen que tener el formato anotacion.nombreclase.propiedad para que spring los interprete como el mensaje que tiene que mostrar, por lo tanto para nuestro ejemplo quedaría así:


Size.producto.codigo = El código del producto tiene que tener entre 3 y 8 caracteres
Pattern.producto.codigo = El código del producto solo puede tener letras mayúsculas o números
NotEmpty.producto.nombre = El nombre del producto es obligatorio
NotNull.producto.precio = El precio es obligatorio
Min.producto.precio = El precio mínimo es 2
Max.producto.precio = El precio máximo es 100

En este ejemplo solo hemos visto algunas de las anotaciones que hay disponibles para validar pero hay unas cuantas más @Email, @NotBlank, @AssertTrue, @Positive, @Negative, @Past, @Future, … y si no encontramos la que necesitamos siempre podemos crear nuestro propio validador.

Añadir validación a los controladores

Una vez que ya tenemos anotada nuestra clase para que se valide tenemos que preparar los métodos de nuestro controller sobre los que queremos que se haga una validación, en nuestro ejemplo vamos a hacerlo sobre guardar.

Las 3 cosas que tenemos que hacer son añadir la anotación @Valid al objeto que queremos validar, en nuestro caso el producto, añadir un parámetro de tipo BindingResult al método y si queremos que en caso de que haya algún error nos mantengamos en el formulario y podamos mostrar los errores haremos la comprobación de si nuestro BindingResult tiene algún error.


@PostMapping("/guardar")
public String guardar(@Valid Producto producto, BindingResult result) {
    if (result.hasErrors()) {			
        return VISTA_FORMULARIO;
    }

    productoService.guardar(producto);

    return "redirect:" + VISTA_LISTA;
}

Mostrar los errores en la vista con Thymeleaf

El último paso que nos queda es mostrarle al usuario cual es el motivo por el que sigue en el formulario y no se ha podido completar la operación que estaba realizando.

Para comprobar en Thymeleaf si hay algun error podemos utilizar ${#fields.hasErrors('*')} si queremos comprobar si hay cualquier error y si queremos comprobar si hay un error en un campo concreto sustituimos el * por el por nombre del campo.

Dependiendo de la forma en la que queramos mostrar los errores también varía la forma en la que tenemos que hacerlo. Si optamos por mostrar todos los errores juntos como una lista lo que haremos será iterar sobre la lista completa de errores del formulario he ir mostrándolos todos como una lista por ejemplo.


<div th:object="${producto}" th:remove="tag">
    <ul th:if="${#fields.hasErrors('*')}" class="alert alert-danger" role="alert">
        <li th:each="error : ${#fields.errors('*')}" th:text="${error}"></li>
    </ul>
</div>

Y si optamos por mostrar los errores para cada uno de los campos por separado podemos hacerlo con th:errors="*{nombrePropiedad}" que devuelve todos los errores asociados a cada propiedad.


<form th:action="@{guardar}" th:object="${producto}" method="post">
    <input type="hidden" th:field="*{id}" />

    <div class="form-group row">
        <label class="col-sm-2 col-form-label">Código</label>
        <div class="col-sm-6">
            <input type="text" th:field="*{codigo}">
            <small class="form-text text-danger" th:if="${#fields.hasErrors('codigo')}" th:errors="*{codigo}"></small>
        </div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2 col-form-label">Nombre</label>
        <div class="col-sm-6">
            <input type="text" th:field="*{nombre}">
            <small class="form-text text-danger" th:if="${#fields.hasErrors('nombre')}" th:errors="*{nombre}"></small>
        </div>
    </div>
    <div class="form-group row">
        <label class="col-sm-2 col-form-label">Precio</label>
        <div class="col-sm-6">
            <input type="text" th:field="*{precio}">
            <small class="form-text text-danger" th:if="${#fields.hasErrors('precio')}" th:errors="*{precio}"></small>
        </div>
    </div>
    <div class="form-group">
        <div class="col-sm-6">
            <input type="submit" value="Añadir producto" class="btn btn-outline-primary">
            <a th:href="@{lista}" class="btn btn-outline-danger">Cancelar</a>
        </div>
    </div>
</form>

Y el resultado de cómo se visualizan los errores de esta segunda forma es este:

Si quieres probar el código tal y como estaba en el momento correspondiente a este post puedes hacerlo desde este enlace y si prefieres echarle un vistazo a como se encuentra en este momento o al repositorio en general pues puedes hacerlo aquí.