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