Spring ofrece dos mecanismos principales para validar datos de entrada: @Valid, definido por Bean Validation, y @Validated, aportado por Spring. Aunque parecen simliares, tienen alcances distintos.
Diferencias principales
| Aspecto | @Valid | @Valiadted |
|---|---|---|
| Grupos de validación | No los soporta. | Permite aplicar reglas distintas según el grupo indicado. |
| Validación anidada | Puede colocarse sobre atributos para validar objetos contenidos, y combinarse con otras anotaciones de anidamiento. | No puede usarse sobre atributos; solo funciona en clase, método o parámetro. Para objetos internos debe acompañarse de @Valid. |
| Lugares permitidos | Métodos, constructores, parámetros y campos. | Tipos, métodos y parámetros de método. No en campos. |
Como regla general: utiliza @Valid para validación anidada, @Validated cuando necesites grupos de validación y @Validated también para validar parámetros simples en un controlador.
Validación de objetos complejos
Es el caso más frecuente en endpoints POST o PUT que reciben un cuerpo JSON con @RequestBody.
Anotaciones disponibles
// Bean Validation estándar
@Null // el valor debe ser null
@NotNull // el valor no debe ser null
@AssertTrue // debe ser true
@AssertFalse // debe ser false
@Min(1) // número mayor o igual al mínimo
@Max(99) // número menor o igual al máximo
@DecimalMin("0.0")
@DecimalMax("100.0")
@Size(min = 2, max = 50) // tamaño dentro del rango
@Digits(integer = 6, fraction = 2)
@Past // fecha pasada
@Future // fecha futura
@Pattern(regexp = "[A-Z]+")
// Extensiones de Hibernate Validator / Jakarta
@NotBlank // String no null y con texto tras trim()
@Email // formato de correo
@Length(min = 5, max = 255)
@NotEmpty // String, colección, mapa o array no vacío
@Range(min = 1, max = 150)
@NotNull, @NotEmpty y @NotBlank
@NotNull: aplica a cualqueir tipo; solo garantiza que no seanull.@NotBlank: exclusivo paraString; exige que no seanully quetrim()tenga longitud mayor que cero.@NotEmpty: paraString, colecciones, mapas o arrays; exige que no seanully que tenga al menos un elemento o carácter.
Nota: @Pattern solo funciona sobre String. Si se aplica a un Integer u otro tipo se lanzará una excepción indicando que no existe un validador para ese tipo.
Ejemplo básico con @Valid
@Data
public class UserRequest {
@NotBlank(message = "El nombre de usuario es obligatorio")
@Size(max = 30, message = "El nombre no puede superar los 30 caracteres")
private String username;
@NotNull(message = "La edad es obligatoria")
@Min(value = 18, message = "El usuario debe ser mayor de edad")
private Integer age;
@NotBlank(message = "El correo es obligatorio")
@Email(message = "El correo no tiene un formato válido")
private String email;
}
@PostMapping("/users")
public ResponseEntity<?> register(@Valid @RequestBody UserRequest request) {
return ResponseEntity.ok(service.register(request));
}
Importante: la validación no se aplica a variables static. Si la validación falla, Spring lanza MethodArgumentNotValidException (cuando se usa @RequestBody) o BindException (sin @RequestBody). La forma recomendada es capturarlas de forma global.
Manejo centralizado de errores
@RestControllerAdvice
public class GlobalValidationHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleBodyValidation(MethodArgumentNotValidException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.findFirst()
.orElse("Datos de entrada inválidos");
Map<String, Object> response = new HashMap<>();
response.put("status", 400);
response.put("error", message);
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(BindException.class)
public ResponseEntity<Map<String, Object>> handleBinding(BindException ex) {
String message = ex.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.findFirst()
.orElse("Datos de entrada inválidos");
Map<String, Object> response = new HashMap<>();
response.put("status", 400);
response.put("error", message);
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, Object>> handleSingleParam(ConstraintViolationException ex) {
String message = ex.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.findFirst()
.orElse("Parámetro no válido");
Map<String, Object> response = new HashMap<>();
response.put("status", 400);
response.put("error", message);
return ResponseEntity.badRequest().body(response);
}
}
Validación de objetos anidados
Para validar un objeto dentro de otro, coloca @Valid sobre el campo correspondiente:
@Data
public class OrderRequest {
@NotBlank
private String customerName;
@Valid
@NotNull
private AddressRequest shippingAddress;
}
@Data
public class AddressRequest {
@NotBlank
private String city;
@NotBlank
private String street;
@Pattern(regexp = "\\d{5}", message = "Código postal inválido")
private String zipCode;
}
Validación por grupos con @Validated
Cuando un mismo DTO se usa en distintas operaciones (creación, actualización, etc.) y cada una requiere reglas diferentes, se definen interfaces de grupo.
public class ValidationGroups {
public interface Create {}
public interface Update extends Default {}
}
Heredar de Default permite que, al usar el grupo Update, también se ejecuten las validaciones que no especifican grupo (grupo por defecto). Si no se hereda, esas validaciones se omitirán.
@Data
public class ProductRequest {
@NotNull(message = "El id es obligatorio", groups = ValidationGroups.Update.class)
private Long id;
@NotBlank(message = "El nombre es obligatorio", groups = ValidationGroups.Create.class)
@Size(max = 40, message = "El nombre es demasiado largo", groups = ValidationGroups.Create.class)
private String name;
@NotBlank(message = "La descripción no puede estar vacía")
@Size(max = 200, message = "La descripción es demasiado larga")
private String description;
}
@PostMapping("/products")
public ResponseEntity<?> create(@Validated(ValidationGroups.Create.class) @RequestBody ProductRequest request) {
return ResponseEntity.ok(service.create(request));
}
@PutMapping("/products")
public ResponseEntity<?> update(@Validated(ValidationGroups.Update.class) @RequestBody ProductRequest request) {
return ResponseEntity.ok(service.update(request));
}
Validación de parámetros simples
En endpoints GET que reciben valores sueltos con @RequestParam, se coloca @Validated sobre la clase del controlador y las anotaciones de restricción directamente sobre cada parámetro.
@Validated
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping
public ResponseEntity<?> search(
@RequestParam @NotBlank String keyword,
@RequestParam @Min(1) @Max(100) Integer pageSize) {
return ResponseEntity.ok(service.search(keyword, pageSize));
}
}
Validación en la capa de servicio
También es posible validar argumentos de métodos de servicio. Se anota la clase con @Validated y los parámetros que requieran validación de bean con @Valid.
@Validated
@Service
public class OrderService {
public Order create(@Valid OrderRequest request) {
// lógica de negocio
return repository.save(request);
}
}