Aller au contenu principal

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 juste après @Valid

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;
}
AnnotationDescription
@NotNullRefuse null (mais accepte une chaîne vide)
@NotEmptyRefuse null et "" (chaîne vide)
@NotBlankRefuse null, "" et les chaînes composées uniquement d'espaces
@Size(min, max)Longueur d'une chaîne ou taille d'une collection
@EmailVé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
@PositiveDoit être strictement positif
@FutureLa date doit être dans le futur
@PastLa 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') : retourne true si le champ titre a au moins une erreur de validation
  • th:errors="*{titre}" : affiche tous les messages d'erreur pour le champ titre
  • th:classappend : ajoute une classe CSS conditionnellement (ici is-invalid de 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


Quelle annotation déclenche la validation Bean Validation sur un paramètre de méthode ?


Que contient BindingResult après une tentative de validation ?


Quelle est la différence entre @NotEmpty et @NotBlank ?


Que se passe-t-il si BindingResult n'est PAS placé immédiatement après @Valid ?


Quelle syntaxe Thymeleaf affiche les messages d'erreur pour le champ 'titre' ?


Que fait th:classappend="${#fields.hasErrors('titre')} ? 'is-invalid'" ?


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.


Bonne pratique - Messages d'erreur explicites et utiles

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


Bonne pratique - Ne pas sauvegarder si il y a des erreurs

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


Bonne pratique - Surligner visuellement les champs en erreur

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.

📌 Une solution