Spring MVC Validation Groups

Spring MVC Validation Groups

Con los validations groups o grupos de validación se puede conseguir que un objeto se pueda validar de formas distintas dependiendo del lugar desde el que se haga. Esto es muy útil para la validación de formularios por pasos ya que en cada paso solo queremos validar los atributos que aparezcan en cada trozo del formulario o también cuando la validación que hay que hacer es distinta dependiendo de la acción.

En un post anterior vimos como crear validadores personalizados, y en la interfaz de la anotación del validador uno de los métodos que teníamos era Class<?>[] groups() default {};, en los validadores predefinidos es lo mismo, por defecto el grupo del validador es javax.validation.groups.Default a no ser que se defina alguno otro.

Para definir un nuevo grupo simplemente es necesario crear un interface vacío ya sea dentro de una clase o como archivos separados dependiendo de lo genéricos o específicos que sean los grupos y de lo que nos guste más.


public interface Crear {}
public interface Editar {}

Una vez definidos los grupos el siguiente paso es añadir el atributo groups a los validadores que queramos.


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.Null;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import com.programandoointentandolo.tsb.validator.ProductoCodigo;
import com.programandoointentandolo.tsb.validator.ProductoPrecioUnidadesMinimo;
import com.programandoointentandolo.tsb.validator.grupos.Crear;
import com.programandoointentandolo.tsb.validator.grupos.Editar;

@ProductoPrecioUnidadesMinimo
@Entity
public class Producto {

	@Id
	@Null(groups = Crear.class)
	@NotNull(groups = Editar.class)
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;

	@ProductoCodigo
	@Size(min = 3, max = 8)
	@Pattern(regexp = "[A-Z0-9]+")
	private String codigo;

	@NotEmpty
	private String nombre;

	@NotNull
	@Min(value = 2, groups = {Crear.class, Editar.class})
	@Max(100)
	private Double precio;
	
	@NotNull
	@Min(1)
	private Integer unidadesMinimas;

	public Producto() {

	}

	// Getters y Setters

}

En este ejemplo hemos añadido al id el validador @Null en la creación y @NotNull en la edición, si no fuese por los grupos no podríamos estar haciendo estas validaciones porque estaríamos haciendo 2 validaciones opuestas.

Es importante tener en cuenta que si no indicamos ningún grupo en el validador se usa el validador por defecto, pero si indicamos algún grupo el validador solo se ejecutara cuando la validación se corresponda con ese grupo, por lo tanto, en el validador tenemos que indicar todos los grupos con los que queremos que se utilice incluido el validador por defecto si es el caso. Para añadir más de un grupo al validador hay que hacerlo entre {} como en el ejemplo @Min(value = 2, groups = {Crear.class, Editar.class}).

El siguiente paso es sustituir las anotaciones @Valid por @Validated en el controlador para poder pasarle los grupos, aparte de esto nada nuevo.


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

    productoService.guardar(producto);

    return "redirect:" + VISTA_LISTA;
}

@PostMapping("/actualizar")
public String actualizar(@Validated({ Editar.class, Default.class }) Producto producto, BindingResult result) {
    if (result.hasErrors()) {
        return VISTA_FORMULARIO;
    }

    productoService.guardar(producto);

    return "redirect:" + VISTA_LISTA;
}

Al indicar un grupo en la anotacion @Validated es necesario que añadamos también el grupo por defecto con Default.class si queremos que se ejecuten tanto las validaciones especificas del grupo como las genéricas a las que no le hemos puesto ningún grupo.

Y así de sencillo es configurar diferentes validadores para distintas acciones en Spring MVC.

Un apunte final sobre los validadores, si como en el caso de este ejemplo estas utilizando los @Entity en el controlador tienes que tener en cuenta que el objeto se valida 2 veces, primero se valida en el controlador porque lo hemos anotado con @Validated pero al persistirse en base de datos se vuelve a validar nuevamente y como aquí no hemos pasado los grupos en ningún sitio aparte del controlador en esta segunda validación se utilizará el grupo por defecto que puede ser que tenga otras validaciones y que haga que falle la inserción o la actualización, puedes comprobarlo quitando el grupo por defecto de las validaciones del controlador y veras como falla, por eso hay que tener cuidado con los validadores y no definir restricciones más fuertes en base de datos que en el validador de los formularios independientemente de que uses DTOs o las entidades directamente.

Y como siempre añado los cambios que hemos hecho aquí al proyecto para que los puedas probar o revisar, 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í.