Chapitre 2: Validation des Données
Introduction
La validation des données est essentielle pour garantir l'intégrité et la qualité des données dans votre application. Spring Boot intègre Bean Validation (JSR 380) pour valider facilement les données d'entrée.
Bean Validation (JSR 380)
Dépendance Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Note: Cette dépendance est déjà incluse dans spring-boot-starter-web.
Annotations de Validation Principales
Tableau Récapitulatif
| Annotation | Usage | Exemple |
|---|---|---|
@NotNull | Ne peut pas être null | @NotNull private String name; |
@NotEmpty | Ne peut pas être null ou vide | @NotEmpty private String email; |
@NotBlank | Ne peut pas être null, vide ou seulement des espaces | @NotBlank private String title; |
@Size | Taille min/max | @Size(min=3, max=50) private String name; |
@Min | Valeur minimale | @Min(0) private int age; |
@Max | Valeur maximale | @Max(100) private int grade; |
@Email | Format email valide | @Email private String email; |
@Pattern | Expression régulière | @Pattern(regexp="[0-9]{10}") private String phone; |
@Past | Date dans le passé | @Past private LocalDate birthDate; |
@Future | Date dans le futur | @Future private LocalDate appointmentDate; |
Exemple Complet: Entité avec Validation
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 = "Le nom ne peut pas être vide")
@Size(min = 3, max = 50, message = "Le nom doit contenir entre 3 et 50 caractères")
private String name;
@NotNull(message = "Le prix est obligatoire")
@Min(value = 0, message = "Le prix ne peut pas être négatif")
private Double price;
@NotNull(message = "La quantité est obligatoire")
@Min(value = 0, message = "La quantité ne peut pas être négative")
private Integer quantity;
@Email(message = "Email invalide")
private String supplierEmail;
// Constructeurs, getters, setters
public Product() {}
public Product(String name, Double price, Integer quantity) {
this.name = name;
this.price = price;
this.quantity = quantity;
}
// Getters et 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;
}
}
Utilisation dans le Controller avec @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 avec validation
@PostMapping
public ResponseEntity<Product> createProduct(@Valid @RequestBody Product product) {
Product created = productService.createProduct(product);
return ResponseEntity.status(HttpStatus.CREATED).body(created);
}
// UPDATE avec validation
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@Valid @RequestBody Product product) {
Product updated = productService.updateProduct(id, product);
return ResponseEntity.ok(updated);
}
// GET - Tous
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
return ResponseEntity.ok(productService.getAllProducts());
}
// GET - Par 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();
}
}
Points Clés:
@Validdéclenche la validation automatique- Si la validation échoue, Spring renvoie automatiquement un code 400 Bad Request
Gestion des Erreurs de Validation
Réponse Automatique (par défaut)
Quand la validation échoue, Spring renvoie automatiquement:
{
"timestamp": "2025-11-28T10:15:30.000+00:00",
"status": 400,
"error": "Bad Request",
"message": "Validation failed",
"errors": [
{
"field": "name",
"message": "Le nom ne peut pas être vide"
},
{
"field": "price",
"message": "Le prix ne peut pas être négatif"
}
]
}
Personnalisation avec @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);
}
}
Réponse personnalisée:
{
"name": "Le nom ne peut pas être vide",
"price": "Le prix ne peut pas être négatif"
}
Messages Personnalisés
Approche 1: Directement dans l'annotation
@NotBlank(message = "Le nom du produit est obligatoire")
@Size(min = 3, max = 50, message = "Le nom doit contenir entre {min} et {max} caractères")
private String name;
Approche 2: Fichier de messages (messages.properties)
Fichier src/main/resources/messages.properties:
product.name.notblank=Le nom du produit est obligatoire
product.name.size=Le nom doit contenir entre {min} et {max} caractères
product.price.min=Le prix ne peut pas être inférieur à {value}
Entité:
@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;
Validation Personnalisée
Créer une Annotation Personnalisée
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 "Numéro de téléphone invalide";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Créer le Validateur
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;
}
// Format canadien: (XXX) XXX-XXXX ou XXX-XXX-XXXX
return phoneNumber.matches("\\(?\\d{3}\\)?[- ]?\\d{3}[- ]?\\d{4}");
}
}
Utilisation
@Entity
public class Customer {
@Id
@GeneratedValue
private Long id;
@ValidPhoneNumber(message = "Le numéro de téléphone doit être au format canadien")
private String phoneNumber;
// Getters, setters
}
Groupes de Validation
Permet de valider différemment selon le contexte (création vs mise à jour).
public interface CreateValidation {}
public interface UpdateValidation {}
@Entity
public class Product {
@Id
@GeneratedValue
@Null(groups = CreateValidation.class, message = "L'ID doit être null à la création")
@NotNull(groups = UpdateValidation.class, message = "L'ID est requis pour la mise à jour")
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) {
// ...
}
Résumé du Chapitre
Points Clés
- Bean Validation (JSR 380) pour valider les données
- @Valid dans le controller pour déclencher la validation
- Messages personnalisés dans les annotations ou fichiers properties
- @ControllerAdvice pour gérer les erreurs globalement
- Validation personnalisée avec annotations custom
Annotations Essentielles
✅ @NotNull, @NotEmpty, @NotBlank
✅ @Size, @Min, @Max
✅ @Email, @Pattern
✅ @Past, @Future
✅ @Valid