java

Validar formularios en Spring Boot

Validar formularios con Spring Boot

En este post te mostraré a validar un formulario en Spring Boot y a mostrar los mensajes de error (para indicar en dónde se equivocó al llenarse) con Thymeleaf.

La validación es muy fácil, pues solo se utilizan anotaciones en la entidad.

Verás que es muy sencillo gracias a las anotaciones como Min, NotNull y Size, las cuales se encuentran en:

javax.validation.constraints.*

Primero: la entidad o clase

Dentro de nuestra clase debemos realizar las anotaciones para validar formularios en Spring Boot. Por cada propiedad que haya, hacemos una anotación.

Por ejemplo, veamos esta validación para que la cadena no sea nula:

package hello;
import javax.validation.constraints.NotNull;

public class Producto {
    @NotNull
    private String nombre;
}

En este caso utilizamos la anotación de @NotNull justo arriba de la propiedad llamada nombre.

Las anotaciones también pueden recibir “argumentos”, entre ellos puede ir el mensaje que se muestra en caso de que el dato no sea válido. Por ejemplo:

package hello;
import javax.validation.constraints.NotNull;

public class Producto {
    @NotNull(message = "Debes especificar el nombre")
    private String nombre;
}

A través de message indicamos el mensaje que se muestra en caso de que el dato no sea válido al validar formularios en Spring Boot.

Al final del post dejaré un ejemplo completo de otras validaciones para cada propiedad de la clase.

Validar en el controlador

Una vez que hemos definido las validaciones de la clase, es hora de validar en el controlador los datos enviados por formulario.

Para que la validación se realice, debemos indicar la anotación @Valid al inyectar nuestra entidad o clase, y también debemos inyectar BindingResult.

@PostMapping(value = "/agregar")
public String guardarProducto(@ModelAttribute @Valid Producto producto, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        // Aquí se puede hacer cualquier cosa, yo hago una redirección para mostrar los errores en el form
        return "productos/agregar_producto";
    }
    // Si llegamos hasta aquí es que no hubo errores
    return "redirect:/productos/mostrar";
}

La verdadera comprobación de que los datos son válidos es al llamar a hasErrors(), en caso de que los tenga, se recomienda redirigir al usuario al formulario que llenó de manera equivocada.

Si hasErrors() devuelve false entonces podemos asumir que los datos del formulario son válidos, o mejor dicho, que cumplieron las validaciones indicadas en la entidad.

Mostrar los errores en el formulario

Suponiendo que los datos que el usuario introdujo no son válidos, lo vamos a redireccionar al formulario renderizado con Thymeleaf.

Ahora tenemos disponible a #fields dentro de la vista. En la misma podemos usar th:if  (esto es opcional, pues si usamos th:errors no hace falta el if)con:

#fields.hasErrors("campo")

Para mostrar el mensaje de error usamos:

th:errors="*{campo}"

Los errores de formulario se pueden mostrar en cualquier lugar apropiado. Veamos un ejemplo:

<form method="post">
  <label for="nombre">Nombre del producto</label>
  <input th:field="*{nombre}" id="nombre" placeholder="Escribe el nombre del producto" type="text">
  <div th:errors="*{nombre}"></div>
</form>

En la línea 4 tenemos un div que se mostrará si hay errores en el campo nombre.

Ejemplo completo para validar formularios en Spring Boot

Ejemplo de validación de un formulario con Spring Boot usando Bootstrap 4

Ahora que ya he explicado lo básico, voy a mostrar un ejemplo completo de un sistema que ando haciendo, recuerda que es un ejemplo y que debes entender el código antes de copiarlo y pegarlo.

Comenzando por la clase, que a su vez es una entidad. Se pueden observar todas las validaciones que se hacen a las propiedades, por ejemplo, en el precio o en la existencia.

package hello;

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

@Entity
public class Producto {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @NotNull(message = "Debes especificar el nombre")
    @Size(min = 1, max = 50, message = "El nombre debe medir entre 1 y 50")
    private String nombre;

    @NotNull(message = "Debes especificar el código")
    @Size(min = 1, max = 50, message = "El código debe medir entre 1 y 50")
    private String codigo;

    @NotNull(message = "Debes especificar el precio")
    @Min(value = 0, message = "El precio mínimo es 0")
    private Float precio;

    @NotNull(message = "Debes especificar la existencia")
    @Min(value = 0, message = "La existencia mínima es 0")
    private Float existencia;

    public String getCodigo() {
        return codigo;
    }

    public void setCodigo(String codigo) {
        this.codigo = codigo;
    }

    public Float getPrecio() {
        return precio;
    }

    public void setPrecio(Float precio) {
        this.precio = precio;
    }

    public Float getExistencia() {
        return existencia;
    }

    public void setExistencia(Float existencia) {
        this.existencia = existencia;
    }

    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}

Después tenemos el controlador.

Por el momento solo estoy haciendo la validación del formulario en el método guardarProducto (línea 55) pero como las anotaciones ya están en la clase solo hay que llamara bindingResult.hasErrors en los otros métodos.

package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import javax.validation.Valid;

@Controller
@RequestMapping(path = "/productos")
public class ProductosController {
    @Autowired
    private ProductosRepository productosRepository;

    @GetMapping(value = "/agregar")
    public String agregarProducto(Model model) {
        model.addAttribute("producto", new Producto());
        return "productos/agregar_producto";
    }

    @GetMapping(value = "/mostrar")
    public String mostrarProductos(Model model) {
        model.addAttribute("productos", productosRepository.findAll());
        return "productos/ver_productos";
    }

    @PostMapping(value = "/eliminar")
    public String eliminarProducto(@ModelAttribute Producto producto, RedirectAttributes redirectAttrs) {
        redirectAttrs
                .addFlashAttribute("mensaje", "Eliminado correctamente")
                .addFlashAttribute("clase", "warning");
        productosRepository.deleteById(producto.getId());
        return "redirect:/productos/mostrar";
    }

    @PostMapping(value = "/editar")
    public String actualizarProducto(@ModelAttribute Producto producto, RedirectAttributes redirectAttrs) {
        productosRepository.save(producto);
        redirectAttrs
                .addFlashAttribute("mensaje", "Editado correctamente")
                .addFlashAttribute("clase", "success");
        return "redirect:/productos/mostrar";
    }

    @GetMapping(value = "/editar/{id}")
    public String mostrarFormularioEditar(@PathVariable int id, Model model) {
        model.addAttribute("producto", productosRepository.findById(id));
        return "productos/editar_producto";
    }

    @PostMapping(value = "/agregar")
    public String guardarProducto(@ModelAttribute @Valid Producto producto, BindingResult bindingResult, RedirectAttributes redirectAttrs) {
        if (bindingResult.hasErrors()) {
            return "productos/agregar_producto";
        }
        productosRepository.save(producto);
        redirectAttrs
                .addFlashAttribute("mensaje", "Agregado correctamente")
                .addFlashAttribute("clase", "success");
        return "redirect:/productos/mostrar";
    }
}

Finalmente en la vista tenemos el siguiente formulario. Se agrega la clase dinámica is-invalid en caso de que haya errores, y también se muestra un div con ese mensaje usando th:errors.

<!DOCTYPE html>
<html lang="es" xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="master">
<body>
<main layout:fragment="contenido">
    <div class="col-12">
        <h2>Agregar producto</h2>
        <form th:object="${producto}" th:action="@{/productos/agregar}" method="post">
            <div class="form-group">
                <label for="nombre">Nombre del producto</label>
                <input th:field="*{nombre}" id="nombre" placeholder="Escribe el nombre del producto" type="text"
                       class="form-control" th:classappend="${#fields.hasErrors('nombre')} ? 'is-invalid' : ''">
                <div class="invalid-feedback" th:errors="*{nombre}"></div>
            </div>
            <div class="form-group">
                <label for="codigo">Código de barras</label>
                <input th:field="*{codigo}" id="codigo" placeholder="Escribe el código del producto" type="text"
                       class="form-control" th:classappend="${#fields.hasErrors('codigo')} ? 'is-invalid' : ''">
                <div class="invalid-feedback" th:errors="*{codigo}"></div>

            </div>
            <div class="form-group">
                <label for="existencia">Existencia actual</label>
                <input th:field="*{existencia}" id="existencia" placeholder="Cantidad actual del producto" type="number"
                       class="form-control" th:classappend="${#fields.hasErrors('existencia')} ? 'is-invalid' : ''">
                <div class="invalid-feedback" th:errors="*{existencia}"></div>

            </div>
            <div class="form-group">
                <label for="existencia">Precio</label>
                <input th:field="*{precio}" id="precio" placeholder="Precio del producto" type="number"
                       class="form-control" th:classappend="${#fields.hasErrors('precio')} ? 'is-invalid' : ''">
                <div class="invalid-feedback" th:errors="*{precio}"></div>

            </div>
            <button class="btn btn-success" type="submit">Guardar</button>
            &nbsp;<a class="btn btn-warning" th:href="@{/productos/mostrar}">Ver todos</a>
        </form>
    </div>
</main>
</body>
</html>

Estoy usando th:classappend para agregar la clase de error al input de Bootstrap. También estoy usando el div con la clase invalid-feedback para que la misma se muestre bajo el input.

Obviamente no necesitas estilos para validar formularios en Spring Boot, pero no pasa nada si los agregas.

Así es como terminamos por hoy. Puedes leer más sobre Spring Boot en mi blog.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Entradas recientes

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

3 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

3 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

3 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

3 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

3 días hace

Solución: Apache – Server unable to read htaccess file

Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…

4 días hace

Esta web usa cookies.