Anotaciones de Validación JSR:
@NullEl elemento anotado debe ser nulo@NotNullEl elemento anotado no debe ser nulo@AssertTrueEl elemento anotado debe ser verdadero@AssertFalseEl elemento anotado debe ser falso@Min(value)El elemento anotado debe ser un número, su valor debe ser mayor o igual al mínimo especificado@Max(value)El elemento anotado debe ser un número, su valor debe ser menor o igual al máximo especificado@DecimalMin(value)El elemento anotado debe ser un número, su valor debe ser mayor o igual al mínimo especificado@DecimalMax(value)El elemento anotado debe ser un número, su valor debe ser menor o igual al máximo especificado@Size(max=, min=)El tamaño del elemento anotado debe estar dentro del rango especificado@Digits (integer, fraction)El elemento anotado debe ser un número, su valor debe estar dentro del rango aceptable@PastEl elemento anotado debe ser una fecha pasada@FutureEl elemento anotado debe ser una fecha futura@Pattern(regex=,flag=)El elemento anotado debe coincidir con la expresión regular especificada
Anotaciones de Validación de Hibernate Validator:
@NotBlank(message =)Verifica que la cadena no sea nula y su longitud debe ser mayor que 0@EmailEl elemento anotado debe ser una dirección de correo electrónico@Length(min=,max=)El tamaño de la cadena anotada debe estar dentro del rango especificado@NotEmptyLa cadena anotada no debe estar vacía@Range(min=,max=,message=)El elemento anotado debe estar dentro del rango adecuado
Configuración de Dependencias
- Para proyectos Spring Boot, se proporciona un starter de validator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
- Para proyectos que no usan Spring Boot, se deben incluir las dependencias manualmente
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>3.0.3</version>
</dependency>
Ejemplo de Entidad con Restricciones
package com.example.modelo;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.*;
@Data
public class Alumno {
@NotBlank(message = "El nombre de usuario no puede estar vacío")
private String nombre;
@Min(value = 18, message = "La edad no puede ser menor a 18 años")
private Integer edad;
@Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "Formato de número de teléfono incorrecto")
private String telefono;
@Email(message = "Formato de correo electrónico incorrecto")
private String email;
@Valid
@NotNull
private Institucion institucion;
@Data
private static class Institucion {
@NotBlank(message = "El nombre de la institución no puede estar vacío")
private String nombre;
@NotBlank(message = "La dirección de la institución no puede estar vacía")
private String direccion;
}
}
Demostración de Validación
Preparar un Controlador para pruebas
package com.example.controlador;
import com.example.modelo.Alumno;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
@RestController
@Slf4j
public class ControladorValidacion {
}
- Cuando el parámetro en un método del Controlador es un objeto (como Alumno, y los campos ya tienen restricciones), si no se usa la anotación @Validated, las restricciones no surten efecto
@RestController
@Slf4j
public class ControladorValidacion {
@GetMapping("/prueba1")
public String prueba1(Alumno alumno) {
log.info("Información del alumno:{}", alumno);
return "ok";
}
}
- Después de añadir la anotación @Validated al parámetro del objeto, las restricciones surten efecto
@RestController
@Slf4j
public class ControladorValidacion {
@GetMapping("/prueba2")
public String prueba2(@Validated Alumno alumno) {
log.info("Información del alumno:{}", alumno);
return "ok";
}
@GetMapping("/prueba3")
public String prueba3(@Validated @RequestBody Alumno alumno) {
log.info("Información del alumno:{}", alumno);
return "ok";
}
}
2023-05-15 10:20:45.123 WARN 5678 --- [nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 5 errors
Field error in object 'alumno' on field 'nombre': rejected value []; codes [NotBlank.alumno.nombre,NotBlank.nombre,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [alumno.nombre,nombre]; arguments []; default message [nombre]]; default message [El nombre de usuario no puede estar vacío]
Field error in object 'alumno' on field 'institucion.nombre': rejected value []; codes [NotBlank.alumno.institucion.nombre,NotBlank.institucion.nombre,NotBlank.nombre,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [alumno.institucion.nombre,institucion.nombre]; arguments []; default message [institucion.nombre]]; default message [El nombre de la institución no puede estar vacío]
Field error in object 'alumno' on field 'institucion.direccion': rejected value []; codes [NotBlank.alumno.institucion.direccion,NotBlank.institucion.direccion,NotBlank.direccion,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [alumno.institucion.direccion,institucion.direccion]; arguments []; default message [direccion]]; default message [La dirección de la institución no puede estar vacía]
Field error in object 'alumno' on field 'edad': rejected value [15]; codes [Min.alumno.edad,Min.edad,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [alumno.edad,edad]; arguments []; default message [edad],18]; default message [La edad no puede ser menor a 18 años]
Field error in object 'alumno' on field 'telefono': rejected value []; codes [Pattern.alumno.telefono,Pattern.telefono,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [alumno.telefono,telefono]; arguments []; default message [telefono],[Ljavax.validation.constraints.Pattern$Flag;@3f4e9a7b,^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\d{8}$]; default message [Formato de número de teléfono incorrecto]]
- La validación también funciona con parámetros en formato JSON
@RestController
@Slf4j
public class ControladorValidacion {
@PostMapping("/prueba4")
public String prueba4(@Validated @RequestBody Alumno alumno) {
log.info("Información del alumno:{}", alumno);
return "ok";
}
}
2023-05-15 10:22:30.456 WARN 5678 --- [nio-8080-exec-7] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.example.controlador.ControladorValidacion.prueba4(com.example.modelo.Alumno) with 2 errors: [Field error in object 'alumno' on field 'nombre': rejected value []; codes [NotBlank.alumno.nombre,NotBlank.nombre,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [alumno.nombre,nombre]; arguments []; default message [nombre]]; default message [El nombre de usuario no puede estar vacío]] [Field error in object 'alumno' on field 'telefono': rejected value [12345678]; codes [Pattern.alumno.telefono,Pattern.telefono,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [alumno.telefono,telefono]; arguments []; default message [telefono],[Ljavax.validation.constraints.Pattern$Flag;@5a7b8c9d,^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\d{8}$]; default message [Formato de número de teléfono incorrecto]] ]
- Si se desea validar directamente los parámetros en métodos del Controlador, es necesario añadir @Validated a la clase, de lo contrario las restricciones no surten efecto
@RestController
@Slf4j
public class ControladorValidacion {
@GetMapping("/prueba5")
public String prueba5(@NotBlank(message = "El nombre no puede estar vacío") String nombre,
@Min(value = 18, message = "La edad no puede ser menor a 18 años") Integer edad) {
log.info("El estudiante {} tiene {} años", nombre, edad);
return "ok";
}
}
2023-05-15 10:25:15.789 INFO 5678 --- [nio-8080-exec-3] com.example.controlador.ControladorValidacion : El estudiante null tiene 16 años
- Al añadir @Validated a la clase, las restricciones surten efecto
@RestController
@Slf4j
@Validated
public class ControladorValidacion {
@GetMapping("/prueba6")
public String prueba6(@NotBlank(message = "El nombre no puede estar vacío") String nombre,
@Min(value = 18, message = "La edad no puede ser menor a 18 años") Integer edad) {
log.info("El estudiante {} tiene {} años", nombre, edad);
return "ok";
}
}
2023-05-15 10:27:22.345 ERROR 5678 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: prueba6.nombre: El nombre no puede estar vacío, prueba6.edad: La edad no puede ser menor a 18 años] with root cause
javax.validation.ConstraintViolationException: prueba6.nombre: El nombre no puede estar vacío, prueba6.edad: La edad no puede ser menor a 18 años
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.8.jar:5.3.8]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.8.jar:5.3.8]....
@Validated vs @Valid
La anotación @Valid y @Validated tienen funcionalidades similares, pero con diferencias:
- @Valid pertenece al paquete javax, mientras que @Validated pertenece a Spring
- @Valid soporta validación anidada, mientras que @Validated no
- @Validated soporta grupos de validación, mientras que @Valid no
Creación de Anotaciones de Validación Personalizadas
- Crear una anotación personalizada @Telefono
package com.example.anotacion;
import com.example.validador.ValidadorTelefono;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* Anotación personalizada: valida el formato del número de teléfono
*/
@Documented
@Constraint(validatedBy = ValidadorTelefono.class)
@Target({ElementType.METHOD, ElementType.FIELD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Telefono {
String message() default "¡El formato del teléfono no es correcto!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
- Definir el validador específico
package com.example.validador;
import com.example.anotacion.Telefono;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Validador personalizado para la anotación @Telefono
*/
public class ValidadorTelefono implements ConstraintValidator<Telefono, String> {
@Override
public boolean isValid(String telefono, ConstraintValidatorContext context) {
// Si el usuario no ingresa nada, no se valida, ya que la comprobación de nulo se deja a @NotNull
if (telefono == null || telefono.isEmpty()) {
return true;
}
Pattern patron = Pattern.compile("^(13[0-9]|14[5|7|9]|15[0|1|2|3|5|6|7|8|9]|17[0|1|6|7|8]|18[0-9])\\d{8}$");
// Si la validación pasa, devuelve true; de lo contrario, devuelve false
Matcher comparador = patron.matcher(telefono);
return comparador.matches();
}
@Override
public void initialize(Telefono constraintAnnotation) {
}
}
- Uso y prueba
@RestController
@Slf4j
@Validated
public class ControladorValidacion {
@GetMapping("/validarTelefono")
public String validarTelefono(@Telefono(message = "Por favor, ingrese un número de teléfono válido") String numeroTelefono) {
log.info("Número de teléfono del estudiante:{}", numeroTelefono);
return "ok";
}
}
2023-05-15 10:35:40.123 ERROR 5678 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: validarTelefono.numeroTelefono: Por favor, ingrese un número de teléfono válido] with root cause
javax.validation.ConstraintViolationException: validarTelefono.numeroTelefono: Por favor, ingrese un número de teléfono válido
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120) ~[spring-context-5.3.8.jar:5.3.8]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.8.jar:5.3.8]....
Validación de Modelos con Múltiples Niveles Anidados
Como se vio anteriormente, la propiedad institución en el objeto Alumno también es un objeto. Para que las restricciones en nombre y dirección de Institución surtan efecto, se debe añadir la anotación @Valid a la propiedad institución, y también se debe añadir @NotNull para evitar que el objeto institución sea nulo. Como se mencionó antes, @Valid soporta validación anidada, mientras que @Validated no.
package com.example.modelo;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.*;
@Data
public class Alumno {
@NotBlank(message = "El nombre de usuario no puede estar vacío")
private String nombre;
@Min(value = 18, message = "La edad no puede ser menor a 18 años")
private Integer edad;
@Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "Formato de número de teléfono incorrecto")
private String telefono;
@Email(message = "Formato de correo electrónico incorrecto")
private String email;
@Valid
@NotNull
private Institucion institucion;
@Data
private static class Institucion {
@NotBlank(message = "El nombre de la institución no puede estar vacío")
private String nombre;
@NotBlank(message = "La dirección de la institución no puede estar vacía")
private String direccion;
}
}
Validación por Grupos
En el desarrollo de aplicaciones, a menudo necesitamos validaciones diferentes para operaciones de creación y modificación. Por ejemplo, al crear un nuevo usuario, no hay ID de usuario, pero al modificar un usuario, se debe proporcionar el ID. En estos casos, podemos usar la validación por grupos.
- Definir dos grupos, Crear y Modificar
package com.example.configuracion;
import javax.validation.groups.Default;
/**
* Grupo de validación para creación
*/
public interface Crear extends Default {
}
package com.example.configuracion;
import javax.validation.groups.Default;
/**
* Grupo de validación para modificación
*/
public interface Modificar extends Default {
}
- Especificar el grupo en la propiedad ID del modelo Alumno
@Data
public class Alumno {
@NotBlank(message = "El ID no puede estar vacío", groups ={Modificar.class} )
private String id;
@NotBlank(message = "El nombre de usuario no puede estar vacío")
private String nombre;
@Min(value = 18, message = "La edad no puede ser menor a 18 años")
private Integer edad;
@Telefono
private String telefono;
@Email(message = "Formato de correo electrónico incorrecto")
private String email;
@Valid
@NotNull
private Institucion institucion;
@Data
private static class Institucion {
@NotBlank(message = "El nombre de la institución no puede estar vacío")
private String nombre;
@NotBlank(message = "La dirección de la institución no puede estar vacía")
private String direccion;
}
}
- Al iniciar la validación en el Controlador, especificar el grupo de validación
@PostMapping("/crearAlumno")
public String crearAlumno(@Validated @RequestBody Alumno alumno) {
log.info("Información del alumno:{}", alumno);
return "¡Alumno creado exitosamente!";
}
@PostMapping("/modificarAlumno")
public String modificarAlumno(@Validated(value = Modificar.class) @RequestBody Alumno alumno) {
log.info("Información del alumno:{}", alumno);
return "¡Alumno modificado exitosamente!";
}
2023-05-15 10:40:15.789 INFO 5678 --- [nio-8080-exec-3] com.example.controlador.ControladorValidacion : Información del alumno:Alumno(id=, nombre=xxxxxx@gmail.com, edad=18, telefono=18158872278, email=xxxxxx@gmail.com, institucion=Alumno.Institucion(nombre=Universidad XYZ, dirección=Calle Principal 123))
2023-05-15 10:41:30.456 WARN 5678 --- [nio-8080-exec-5] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.example.controlador.ControladorValidacion.modificarAlumno(com.example.modelo.Alumno): [Field error in object 'alumno' on field 'id': rejected value []; codes [NotBlank.alumno.id,NotBlank.id,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [alumno.id,id]; arguments []; default message [id]]; default message [El ID no puede estar vacío]] ]
Sobre la herencia de grupos personalizados
La herencia del grupo Default no es obligatoria.
- Si se hereda de Default, el ámbito de validación de @Validated(value = Crear.class) incluye [Crear] y [Default]
- Si no se hereda de Default, el ámbito de validación de @Validated(value = Crear.class) incluye solo [Crear]
Las propiedades como nombre, edad, teléfono en el objeto Alumno tienen como grupo predeterminado Default. Si el grupo personalizado no hereda de Default, al especificar el grupo de validación en el método del Controlador, se debe incluir el grupo Default; de lo contrario, las restricciones en propiedades como nombre, edad, teléfono no surtirán efecto.
Manejo de Excepciones de Vlaidación
Cuando la validación de anotaciones no pasa, devolver directamente los mensajes de excepción al frontend no es amigable. Podemos empaquetar y procesar las excepciones para devolver información más adecuada al frontend.
- Opción 1: Usar la clase BindingResult
@PostMapping("/procesarValidacion")
public String procesarValidacion(@Validated @RequestBody Alumno alumno, BindingResult resultado) {
if (resultado.hasErrors()){
List<ObjectError> todosErrores = resultado.getAllErrors();
StringBuilder constructor = new StringBuilder();
todosErrores.forEach(error->{
log.error(error.getDefaultMessage());
constructor.append("["+error.getDefaultMessage()+"]");
});
return constructor.toString();
}
log.info("Información del alumno:{}", alumno);
return "ok";
}