Adloun

Validation et gestion des erreurs

Cours · Spring Boot : écrire le serveur (école d'ingénieur), chapitre 4 · école d'ingénieur, informatique · le début du chapitre, la suite sur Adloun

L'API du chapitre précédent accepte n'importe quoi : un livre sans titre, un prix négatif, un stock de moins trois exemplaires. Et quand elle refuse une requête, elle le fait mal : un 400 sans explication, un 404 au format maison. Or l'application Vue doit pouvoir dire au visiteur ce qui ne va pas dans son formulaire, champ par champ. Il manque aussi la fonction la plus importante d'une librairie : prendre des commandes, en refusant celles qui demandent plus d'exemplaires qu'il n'y en a en stock.

Ce chapitre ajoute ces vérifications et donne à toutes les erreurs de l'API un format unique et standard. Les règles simples (un champ obligatoire, une valeur minimale, un format) se déclarent par des annotations sur les records de requête : c'est Bean Validation. Les règles qui demandent de consulter le catalogue sont vérifiées par les services, qui signalent un refus par une exception. Une classe unique traduit enfin toutes ces erreurs en réponses HTTP au format ProblemDetail.

Objectifs du chapitre

  • Valider les données reçues avec les annotations de Bean Validation et @Valid.
  • Écrire des messages d'erreur en français et une contrainte personnalisée.
  • Valider les paramètres d'une méthode de contrôleur.
  • Signaler un refus métier par une exception, et traduire toutes les exceptions en un seul endroit, avec @RestControllerAdvice.
  • Répondre au format standard ProblemDetail (RFC 9457), sans jamais divulguer de détail technique.
  • Accepter les commandes de l'application Vue.

Deux projets accompagnent le chapitre : la Librairie (code/ch4/librairie) et un petit projet qui montre ce que fait Spring par défaut (code/ch4/validation-brute). Toutes les réponses montrées ont été obtenues sur ces applications en marche.

4.1 Ne jamais faire confiance au client

Le formulaire de commande de l'application Vue vérifie déjà que le nom est rempli et que l'adresse électronique a la bonne forme. Pourquoi recommencer sur le serveur ? Parce que le serveur ne sait pas qui l'appelle. Une requête peut venir de notre application, d'une ancienne version de celle-ci restée en cache, d'un script, ou de quelqu'un qui tape une commande curl pour voir ce qui se passe si l'on commande livres.

Proposition 4.1Deux validations, deux rôles

La validation du client sert le confort de l'utilisateur : elle répond instantanément, sans aller-retour réseau. La validation du serveur sert la sûreté des données : elle seule est garantie, puisque le client peut être contourné. Toute règle importante est donc vérifiée par le serveur, même si le client la vérifie aussi.

4.2 Bean Validation

Bean Validation (Jakarta Validation) est une spécification standard de Java : on décrit les règles par des annotations sur les champs, et un validateur, ici Hibernate Validator, les vérifie. Spring Boot l'intègre avec le starter spring-boot-starter-validation, à ajouter au pom.xml (dépendance « Validation » d'Initializr).

Commençons par un exemple minimal, sans autre précaution. Un record de requête porte deux contraintes, et le paramètre de la méthode est annoté @Valid :


public record NouveauLivre(
        @NotBlank String titre,
        @Positive double prix) {
}

@RestController
public class NouveauLivreController {

    @PostMapping("/api/livres")
    public NouveauLivre creer(@Valid @RequestBody NouveauLivre livre) {
        return livre;
    }
}

@Valid demande à Spring de valider l'objet construit à partir du corps avant d'appeler la méthode. Si une contrainte échoue, la méthode n'est pas appelée. Le client reçoit :


$ curl -i -X POST localhost:8080/api/livres \
    -H 'Content-Type: application/json' -d '{"titre": "", "prix": -5}'
HTTP/1.1 400 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 02 Oct 2026 21:33:43 GMT
Connection: close

{"timestamp":"2026-10-02T21:33:43.763Z","status":400,"error":"Bad Request","path":"/api/livres"}

C'est tout : un 400 sans un mot d'explication. Les messages existent pourtant, mais seulement dans le journal du serveur :


WARN  [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public fr.librairie.NouveauLivre fr.librairie.NouveauLivreController.creer(fr.librairie.NouveauLivre) with 2 errors: [Field error in object 'nouveauLivre' on field 'titre': rejected value []; codes [NotBlank.nouveauLivre.titre,NotBlank.titre,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [nouveauLivre.titre,titre]; arguments []; default message [titre]]; default message [ne doit pas être vide]] [Field error in object 'nouveauLivre' on field 'prix': rejected value [-5.0]; codes [Positive.nouveauLivre.prix,Positive.prix,Positive.double,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [nouveauLivre.prix,prix]; arguments []; default message [prix]]; default message [doit être supérieur à 0]] ]

On y lit, au milieu de détails techniques, les messages par défaut des deux contraintes, en français : ne doit pas être vide et doit être supérieur à 0. Leur langue dépend de la requête : Spring choisit celle qu'annonce l'en-tête Accept-Language et, à défaut, celle du serveur. Les deux mêmes requêtes, avec et sans cet en-tête :


sans Accept-Language (serveur en français) : « ne doit pas être vide », « doit être supérieur à 0 »
avec Accept-Language: en                  : « must not be blank », « must be greater than 0 »

Il reste à renvoyer ces messages au client, dans des termes choisis. Avant cela, faisons le tour des contraintes disponibles.

AnnotationExige que la valeur…
@NotNullsoit présente
@NotBlanksoit une chaîne avec au moins un caractère non blanc
@NotEmptysoit une chaîne, une liste… non vide
@Size(min, max)ait une longueur ou une taille comprise entre les bornes
@Min, @Maxsoit un entier au moins, au plus égal à une borne
@DecimalMin, @DecimalMaxidem pour un nombre décimal
@Positive, @PositiveOrZerosoit strictement positive, positive ou nulle
@Pattern(regexp)corresponde entièrement à une expression régulière
@Emailait la forme d'une adresse électronique
@Past, @Futuresoit une date passée, future
@AssertTruevaille true

null est accepté par presque toutes les contraintes

Sauf @NotNull, @NotBlank et @NotEmpty, toutes les contraintes considèrent qu'une valeur absente est valide : un @Pattern seul laisse passer un champ manquant. On ajoute donc @NotNull quand le champ est obligatoire. C'est voulu : chaque contrainte vérifie une seule chose.

4.3 Valider les livres de la Librairie

Le record de requête de la Librairie reçoit ses contraintes, chacune avec son message en français :

La suite du cours est sur Adloun

Le chapitre en entier, formules et figures comprises, avec le tuteur, les fiches, les QCM et les exercices.

Lire la suite du cours

En accès libre, sans inscription.

Exercices corrigés de ce chapitre

Lire la suite du cours →