Capítulo 2: Validación de Datos
Introducción
La validación de datos es esencial para garantizar la integridad y la calidad de los datos en su aplicación. Spring Boot integra Bean Validation (JSR 380) para validar fácilmente los datos de entrada.
Bean Validation (JSR 380)
Dependencia Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Nota: Esta dependencia ya está incluida en spring-boot-starter-web.
Anotaciones Principales de Validación
Tabla Resumen
| Anotación | Uso | Ejemplo |
|---|---|---|
@NotNull | No puede ser null | @NotNull private String name; |
@NotEmpty | No puede ser null ni vacío | @NotEmpty private String email; |
@NotBlank | No puede ser null, vacío o solo espacios | @NotBlank private String title; |
@Size | Tamaño mínimo/máximo | @Size(min=3, max=50) private String name; |
@Min | Valor mínimo | @Min(0) private int age; |
@Max | Valor máximo | @Max(100) private int grade; |
@Email | Formato de email válido | @Email private String email; |
@Pattern | Expresión regular | @Pattern(regexp="[0-9]{10}") private String phone; |
@Past | Fecha en el pasado | @Past private LocalDate birthDate; |
@Future | Fecha en el futuro | @Future private LocalDate appointmentDate; |
Ejemplo Completo: Entidad con Validación
package com.example.demo.model;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "El nombre no puede estar vacío")
@Size(min = 3, max = 50, message = "El nombre debe tener entre 3 y 50 caracteres")
private String name;
@NotNull(message = "El precio es obligatorio")
@Min(value = 0, message = "El precio no puede ser negativo")
private Double price;
@NotNull(message = "La cantidad es obligatoria")
@Min(value = 0, message = "La cantidad no puede ser negativa")
private Integer quantity;
@Email(message = "Email inválido")
private String supplierEmail;
// Constructores, getters, setters
public Product() {}
public Product(String name, Double price, Integer quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
// Getters y Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Integer getQuantity() {
return quantity;
}
public void setQuantity(Integer quantity) {
this.quantity = quantity;
}
public String getSupplierEmail() {
return supplierEmail;
}
public void setSupplierEmail(String supplierEmail) {
this.supplierEmail = supplierEmail;
}
}
Uso en el Controller con @Valid
package com.example.demo.controller;
import com.example.demo.model.Product;
import com.example.demo.service.ProductService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
// CREATE con validación
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// UPDATE con validación
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@Valid @RequestBody Product product) {
Product updated = productService.updateProduct(id, product);
return ResponseEntity.ok(updated);
}
// GET - Todos
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
return ResponseEntity.ok(productService.getAllProducts());
}
// GET - Por ID
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Product product = productService.getProductById(id);
return ResponseEntity.ok(product);
}
// DELETE
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
Puntos Clave:
@Validactiva la validación automática- Si la validación falla, Spring devuelve automáticamente un código 400 Bad Request
Manejo de Errores de Validación
Respuesta Automática (por defecto)
Cuando la validación falla, Spring devuelve automáticamente:
{
"timestamp": "2025-11-28T10:15:30.000+00:00",
"status": 400,
"error": "Bad Request",
"message": "Validation failed",
"errors": [
{
"field": "name",
"message": "El nombre no puede estar vacío"
},
{
"field": "price",
"message": "El precio no puede ser negativo"
}
]
}
Personalización con @ControllerAdvice
package com.example.demo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}
Respuesta personalizada:
{
"name": "El nombre no puede estar vacío",
"price": "El precio no puede ser negativo"
}
Mensajes Personalizados
Enfoque 1: Directamente en la anotación
@NotBlank(message = "El nombre del producto es obligatorio")
@Size(min = 3, max = 50, message = "El nombre debe tener entre {min} y {max} caracteres")
private String name;
Enfoque 2: Archivo de mensajes (messages.properties)
Archivo src/main/resources/messages.properties:
product.name.notblank=El nombre del producto es obligatorio
product.name.size=El nombre debe tener entre {min} y {max} caracteres
product.price.min=El precio no puede ser inferior a {value}
Entidad:
@NotBlank(message = "{product.name.notblank}")
@Size(min = 3, max = 50, message = "{product.name.size}")
private String name;
@Min(value = 0, message = "{product.price.min}")
private Double price;
Validación Personalizada
Crear una Anotación Personalizada
package com.example.demo.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPhoneNumber {
String message() default "Número de teléfono inválido";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Crear el Validador
package com.example.demo.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class PhoneNumberValidator implements ConstraintValidator<ValidPhoneNumber, String> {
@Override
public void initialize(ValidPhoneNumber constraintAnnotation) {
}
@Override
public boolean isValid(String phoneNumber, ConstraintValidatorContext context) {
if (phoneNumber == null) {
return false;
}
// Formato canadiense: (XXX) XXX-XXXX o XXX-XXX-XXXX
return phoneNumber.matches("\\(?\\d{3}\\)?[- ]?\\d{3}[- ]?\\d{4}");
}
}
Uso
@Entity
public class Customer {
@Id
@GeneratedValue
private Long id;
@ValidPhoneNumber(message = "El número de teléfono debe tener formato canadiense")
private String phoneNumber;
// Getters, setters
}
Grupos de Validación
Permite validar de manera diferente según el contexto (creación vs actualización).
public interface CreateValidation {}
public interface UpdateValidation {}
@Entity
public class Product {
@Id
@GeneratedValue
@Null(groups = CreateValidation.class, message = "El ID debe ser null al crear")
@NotNull(groups = UpdateValidation.class, message = "El ID es requerido para la actualización")
private Long id;
@NotBlank(groups = {CreateValidation.class, UpdateValidation.class})
private String name;
}
// Controller
@PostMapping
public ResponseEntity<Product> create(@Validated(CreateValidation.class) @RequestBody Product product) {
// ...
}
@PutMapping("/{id}")
public ResponseEntity<Product> update(@PathVariable Long id,
@Validated(UpdateValidation.class) @RequestBody Product product) {
// ...
}
Resumen del Capítulo
Puntos Clave
- Bean Validation (JSR 380) para validar los datos
- @Valid en el controller para activar la validación
- Mensajes personalizados en las anotaciones o archivos properties
- @ControllerAdvice para manejar errores globalmente
- Validación personalizada con anotaciones personalizadas
Anotaciones Esenciales
✅ @NotNull, @NotEmpty, @NotBlank
✅ @Size, @Min, @Max
✅ @Email, @Pattern
✅ @Past, @Future
✅ @Valid