Saltar al contenido principal

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ónUsoEjemplo
@NotNullNo puede ser null@NotNull private String name;
@NotEmptyNo puede ser null ni vacío@NotEmpty private String email;
@NotBlankNo puede ser null, vacío o solo espacios@NotBlank private String title;
@SizeTamaño mínimo/máximo@Size(min=3, max=50) private String name;
@MinValor mínimo@Min(0) private int age;
@MaxValor máximo@Max(100) private int grade;
@EmailFormato de email válido@Email private String email;
@PatternExpresión regular@Pattern(regexp="[0-9]{10}") private String phone;
@PastFecha en el pasado@Past private LocalDate birthDate;
@FutureFecha 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:

  • @Valid activa 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


Capítulo siguiente: Gestión de Errores HTTP

Volver al Módulo 3