Formulaires et validation
Notions théoriques
Liaison formulaire ↔ objet Java avec @ModelAttribute
@ModelAttribute dans un paramètre de méthode demande à Spring de construire un objet Java à partir des données du formulaire HTML. Spring fait correspondre les attributs name des champs HTML avec les noms des propriétés Java (via les setters).
@PostMapping("/articles/nouveau")
public String sauvegarder(@ModelAttribute Article article) {
// Spring a rempli article.titre, article.contenu, etc.
// à partir des champs name="titre", name="contenu" du formulaire
articleRepository.save(article);
return "redirect:/articles";
}
Déclencher la validation avec @Valid
Pour activer la validation Bean Validation, ajoutez @Valid devant le paramètre @ModelAttribute et un paramètre BindingResult juste après :
@PostMapping("/articles/nouveau")
public String sauvegarder(@Valid @ModelAttribute Article article,
BindingResult result) {
if (result.hasErrors()) {
// Il y a des erreurs : réafficher le formulaire avec les messages
return "articles/nouveau";
}
articleRepository.save(article);
return "redirect:/articles";
}
BindingResult doit être placé immédiatement après le paramètre annoté @Valid. Si vous mettez d'autres paramètres entre les deux, Spring ne capturera pas les erreurs et lancera une exception automatiquement.
Annotations Bean Validation sur l'entité
Ces annotations (package jakarta.validation.constraints) sont placées directement sur les attributs de l'entité Java :
import jakarta.validation.constraints.*;
@Entity
public class Article {
@NotBlank(message = "Le titre est obligatoire")
@Size(min = 5, max = 200, message = "Le titre doit faire entre 5 et 200 caractères")
private String titre;
@NotBlank(message = "Le contenu est obligatoire")
private String contenu;
@Email(message = "Format d'email invalide")
private String emailContact;
@Min(value = 0, message = "Doit être positif ou nul")
@Max(value = 100, message = "Doit être inférieur ou égal à 100")
private Integer note;
}
| Annotation | Description |
|---|---|
@NotNull | Refuse null (mais accepte une chaîne vide) |
@NotEmpty | Refuse null et "" (chaîne vide) |
@NotBlank | Refuse null, "" et les chaînes composées uniquement d'espaces |
@Size(min, max) | Longueur d'une chaîne ou taille d'une collection |
@Email | Vérifie le format d'une adresse email |
@Min(value) | Valeur numérique minimale |
@Max(value) | Valeur numérique maximale |
@Pattern(regexp) | Vérifie une expression régulière |
@Positive | Doit être strictement positif |
@Future | La date doit être dans le futur |
@Past | La date doit être dans le passé |
Afficher les erreurs dans Thymeleaf
<form th:action="@{/articles/nouveau}" th:object="${article}" method="post">
<div>
<label for="titre">Titre</label>
<input type="text" id="titre" th:field="*{titre}"
th:classappend="${#fields.hasErrors('titre')} ? 'is-invalid' : ''" />
<!-- Affiche le message d'erreur si le champ titre est invalide -->
<span th:if="${#fields.hasErrors('titre')}"
th:errors="*{titre}"
class="erreur">Message d'erreur</span>
</div>
<div>
<label for="contenu">Contenu</label>
<textarea id="contenu" th:field="*{contenu}"></textarea>
<span th:if="${#fields.hasErrors('contenu')}"
th:errors="*{contenu}"
class="erreur"></span>
</div>
<button type="submit">Publier</button>
</form>
#fields.hasErrors('titre'): retournetruesi le champtitrea au moins une erreur de validationth:errors="*{titre}": affiche tous les messages d'erreur pour le champtitreth:classappend: ajoute une classe CSS conditionnellement (iciis-invalidde Bootstrap)
CSS conditionnel : surligner les champs invalides
<!-- Ajoute la classe CSS "champ-invalide" si le champ a une erreur -->
<input type="text" th:field="*{titre}"
th:classappend="${#fields.hasErrors('titre')} ? 'champ-invalide'" />
Conserver les valeurs saisies après une erreur
Quand le formulaire est réaffiché après une erreur, les valeurs saisies doivent être conservées. Thymeleaf le fait automatiquement grâce à th:field="*{titre}" : il lit la valeur dans l'objet article qui a été repopulé par @ModelAttribute avant la validation.
Exemple pratique
Contrôleur avec validation complète :
package org.joliciel.monblog.controller;
import jakarta.validation.Valid;
import org.joliciel.monblog.entity.Article;
import org.joliciel.monblog.repository.ArticleRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/articles")
public class ArticleController {
private final ArticleRepository articleRepository;
public ArticleController(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}
@GetMapping("/nouveau")
public String afficherFormulaire(Model model) {
model.addAttribute("article", new Article());
return "articles/nouveau";
}
@PostMapping("/nouveau")
public String sauvegarder(@Valid @ModelAttribute Article article,
BindingResult result,
RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
// On renvoie le formulaire avec les erreurs (l'objet article garde les valeurs saisies)
return "articles/nouveau";
}
articleRepository.save(article);
redirectAttributes.addFlashAttribute("message", "Article publié avec succès !");
return "redirect:/articles";
}
}
Entité avec 3 contraintes :
@Entity
@Table(name = "articles")
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Le titre est obligatoire")
@Size(min = 5, max = 200, message = "Le titre doit faire entre 5 et 200 caractères")
@Column(nullable = false, length = 200)
private String titre;
@NotBlank(message = "Le contenu est obligatoire")
@Column(columnDefinition = "TEXT", nullable = false)
private String contenu;
// getters et setters...
}
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Dans ce TP, vous allez ajouter la validation au formulaire de création d'article du projet MonBlog.
Étape 1 — Ajouter les contraintes sur l'entité Article
Annotez les attributs de l'entité Article avec au moins 3 contraintes Bean Validation.
Toujours renseigner le paramètre message dans les annotations de validation. Un message comme "Le titre doit faire entre 5 et 200 caractères" est bien plus utile pour l'utilisateur que le message par défaut "size must be between 5 and 200" en anglais.
Étape 2 — Activer la validation dans le contrôleur
La structure if (result.hasErrors()) { return "vue"; } doit toujours précéder repository.save(). Si vous inversez l'ordre, vous sauvegarderez des données invalides en base avant d'afficher les erreurs.
Étape 3 — Afficher les erreurs dans la vue Thymeleaf
En plus du message d'erreur, colorez le champ invalide en rouge grâce à th:classappend. Cela permet à l'utilisateur d'identifier visuellement les champs à corriger d'un coup d'oeil, sans avoir à lire tous les messages. Avec Bootstrap, la classe is-invalid crée automatiquement un effet visuel rouge.