Validación de parámetros con @Valid y @Validated en Spring Boot

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 sea null.
  • @NotBlank: exclusivo para String; exige que no sea null y que trim() tenga longitud mayor que cero.
  • @NotEmpty: para String, colecciones, mapas o arrays; exige que no sea null y 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);
    }
}

Etiquetas: Spring Boot Bean Validation Hibernate Validator Jakarta Validation Spring MVC

Publicado el 7-14 07:15