CodeGym /Cours /Spring REST & MVC /Stack de validation dans Spring Boot

Stack de validation dans Spring Boot

Spring REST & MVC
Niveau 15, Leçon 1
Disponible

1. Stack de validation : composition et rôle

Si vous avez déjà vu du code où l'on vérifie title à un endroit, assigneeName à un autre, et priority encore ailleurs, vous avez déjà rencontré la « validation par improvisation ». Le stack de validation sert à transformer cette improvisation en mécanisme systématique : des règles claires, un seul moteur de vérification et un endroit prévisible où la requête s'arrête.

Dans le contexte de Spring Boot, nous entendons par stack de validation un ensemble de composants qui permet ceci : vous décrivez les contraintes (constraints) à côté des champs du DTO, Spring lit le JSON dans le DTO, puis exécute automatiquement les vérifications, et si quelque chose ne va pas, la requête se termine avant même d'entrer dans le service. Important : il ne s'agit pas d'une simple annotation @Valid, mais d'une chaîne composée de la norme, de l'implémentation et de l'intégration Spring.

Pour ne pas se perdre, il est utile de garder en tête un « schéma de métro » simple :

Niveau Qu'est‑ce que c'est Responsabilité
Norme (API) Jakarta Bean Validation (jakarta.validation.*) Définit ce que sont @NotBlank, @Size, Validator, ConstraintViolation
Implémentation (moteur) Hibernate Validator Exécute réellement les vérifications selon les annotations
Intégration Spring Spring Boot + Spring MVC Intègre la validation dans le traitement de @RequestBody et transforme les « violations » en scénario d'erreur standard

2. Bean Validation et dépendances

Il est très facile de tomber dans le piège : voir @NotBlank et penser que « c'est Spring qui a inventé ça ». En réalité, Bean Validation est une norme générale de l'écosystème Java (aujourd'hui sous la marque Jakarta), et Spring sait simplement s'y intégrer. C'est une bonne nouvelle : la connaissance est transférable entre les projets et les règles sur les DTO ne dépendent pas du framework utilisé.

Techniquement, l'idée clé de Bean Validation est simple : vous marquez les champs (ou les composants d'un record) avec des annotations de contraintes, puis un objet Validator est capable de vérifier une instance et de retourner la liste des violations (violations). C'est comme avec « un contrôleur dans le métro » : vous pouvez écrire « sans ticket, pas d'entrée » sur la porte autant que vous voulez, mais si vous n'avez pas de contrôleur, la règle reste une philosophie. Le Validator est ce « contrôleur », mais pour les DTO.

Dans un Spring Boot moderne, le paquet de base des annotations ressemble à ceci :

  • jakarta.validation.Valid — « vérifie cet objet »
  • jakarta.validation.constraints.* — @NotNull, @NotBlank, @Size, etc.

Si vous rencontrez des imports javax.validation.*, c'est généralement un vestige du « monde ancien » (et dans la baseline actuelle du cours, mieux vaut ne pas s'habituer à ceux‑ci).

Dépendances : spring-boot-starter-validation

Lorsque vous ajoutez « la validation » dans Spring Boot, vous ajoutez en réalité plusieurs couches à la fois : la norme API, l'implémentation et l'auto‑configuration. C'est pourquoi nous n'utilisons pas un « ensemble aléatoire de bibliothèques » mais un starter normal, comme nous l'avons fait avec spring-boot-starter-webmvc.

Dans notre cours, la baseline pour la validation est définie par la dépendance spring-boot-starter-validation. À l'intérieur (via le BOM Spring Boot) sont tirés à la fois Jakarta Validation API et le fournisseur (dans notre cas, Hibernate Validator). Il est important de comprendre que sans fournisseur, les annotations restent simplement… des annotations. Jolies, inutiles et très sûres d'elles.

Voici à quoi cela ressemble minimalement (extrait de build.gradle.kts) :

dependencies {
    // MVC + Jackson : contrôleurs et conversion JSON
    implementation("org.springframework.boot:spring-boot-starter-webmvc")
    // Bean Validation : API + fournisseur (Hibernate Validator) + auto‑configuration
    implementation("org.springframework.boot:spring-boot-starter-validation")
}

Remarquez la « morale » de cet extrait : le validateur est une partie aussi infrastructurelle de l'application que MVC et Jackson. Nous ne créons pas de Validator à la main, nous ne construisons pas « notre propre roue de vérification de chaînes », nous utilisons ce que Spring Boot sait configurer automatiquement.

Et encore un détail qui fait gagner du temps aux débutants : si vous avez déjà annoté un DTO avec des contraintes, mais que le starter n'est pas déclaré, vous regarderez l'API en pensant que « @Valid ne fonctionne pas ». Il fonctionne. Simplement, il n'y a rien pour vérifier.

3. Validator dans le contexte Spring

La validation dans Spring Boot n'est pas de la « magie sans substance », mais un objet bien concret que l'on peut obtenir comme dépendance. C'est utile psychologiquement : dès que vous avez vu une fois Validator dans un constructeur, votre esprit cesse de percevoir @Valid comme une incantation.

Dans le projet Task Tracker API, nous pouvons (simplement pour comprendre le mécanisme) injecter jakarta.validation.Validator dans n'importe quel composant Spring. Par exemple, dans un petit service de diagnostic :

package com.example.tasktracker.domain.service;

import jakarta.validation.Validator;
import org.springframework.stereotype.Service;

@Service
public class ValidationInspector {

    // C'est le moteur Bean Validation qui sait lire les annotations sur les DTO
    private final Validator validator;

    public ValidationInspector(Validator validator) {
        // Spring place lui‑même le validateur configuré dans le constructeur depuis le contexte
        this.validator = validator;
    }
}

Maintenant, on peut l'appeler manuellement et voir ce qui se passe. Pour le mécanisme, reprenons un fragment réduit du DTO de requête : il suffit de voir quelques contraintes de base, donc assigneeName et priority sont omis ici.

package com.example.tasktracker.api.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record TaskCreateRequest(
        // Title est obligatoire et ne peut pas être une chaîne composée uniquement d'espaces
        @NotBlank
        // On limite la longueur pour que le contrat soit prévisible
        @Size(min = 3, max = 120)
        String title,

        // Description est facultative, mais on limite la taille maximale
        @Size(max = 2000)
        String description
) {}

Et voici une mini‑méthode qui vérifie manuellement le DTO :

import com.example.tasktracker.api.dto.request.TaskCreateRequest;
import jakarta.validation.Validator;

public class Demo {

    static void inspect(Validator validator) {
        // On passe des données délibérément mauvaises : title = espaces, description trop long
        var req = new TaskCreateRequest(" ", "x".repeat(2100));

        // Le validateur lit les annotations sur les composants record et retourne un ensemble de violations
        var violations = validator.validate(req);

        // On attend 2 violations : @NotBlank pour title et @Size(max=2000) pour description
        System.out.println(violations.size()); // 2
    }
}

Le sens d'un tel exemple n'est pas de faire validator.validate() à la main tout le temps (dans le contrôleur, nous n'en avons généralement pas besoin), mais de voir : les règles vivent sur le DTO et le moteur de vérification est unique – que vous le déclenchiez automatiquement via Spring MVC ou manuellement.

4. Validation de @RequestBody dans MVC

Jackson, puis les vérifications

La confusion la plus fréquente chez les débutants est : « puisque j'ai mis @NotBlank, Spring vérifie mon JSON ». Non. Spring vérifie l'objet Java déjà créé après la lecture du corps de la requête. Autrement dit, c'est d'abord HttpMessageConverter (généralement le convertisseur Jackson) qui doit agir, et ensuite seulement Bean Validation.

C'est une compréhension technique très importante, car elle vous permet de prévoir le comportement de l'API dans des scénarios négatifs. Si le JSON est syntaxiquement cassé ou si les types ne correspondent pas, le DTO peut même ne pas apparaître – et par conséquent, la validation n'a « rien à vérifier ». Si en revanche le DTO est créé avec succès, mais que ses valeurs violent les contraintes, alors la validation entre en jeu.

Regardons le schéma (simplifié, mais assez fidèle pour un niveau Junior) :

sequenceDiagram
    participant Client as Client HTTP
    participant MVC as Spring MVC
    participant Conv as "HttpMessageConverter (Jackson)"
    participant Val as "Validator (Bean Validation)"
    participant Ctrl as TaskController
    participant Svc as TaskService

    Client->>MVC: "POST /api/v1/tasks (corps JSON)"
    MVC->>Conv: "lire le corps -> TaskCreateRequest"
    Conv-->>MVC: "objet DTO (ou erreur de lecture)"
    MVC->>Val: "validate(dto) si l'argument est annoté @Valid"
    alt violations présentes
        MVC-->>Client: "400 Bad Request (le contrôleur n'est pas appelé)"
    else OK
        MVC->>Ctrl: "appeler create(dto)"
        Ctrl->>Svc: "taskService.create(dto)"
        Svc-->>Ctrl: "DTO de réponse"
        Ctrl-->>Client: "201 Created + JSON"
    end

Deux points d'arrêt principaux sont à distinguer :

Le premier se situe à l'étape de conversion du corps de la requête en DTO. Si le corps ne peut pas être lu, on ne peut aller plus loin car le paramètre de la méthode du contrôleur ne sera pas construit.

Le second se situe à l'étape de validation. Le DTO existe, mais il est « mauvais » selon les règles du contrat.

Pour nous aujourd'hui, le principal est : Bean Validation concerne le second cas, pas le premier.

JSON malformé et contraintes violées

Dans la conception d'API, il est très facile de regrouper toutes les mauvaises requêtes dans un même panier « 400 et basta ». Mais même au niveau de la compréhension du mécanisme, il est important de distinguer deux familles de problèmes : « impossible à lire » et « lu, mais pas accepté ».

Imaginons deux requêtes vers POST /api/v1/tasks.

La première – le JSON est syntaxiquement cassé (guillemet manquant, virgule superflue, etc.). Par exemple :

POST http://localhost:8080/api/v1/tasks
Content-Type: application/json

{
  "title": "Buy milk,
  "description": "Remember lactose-free"
}

Ici, HttpMessageConverter ne peut même pas construire TaskCreateRequest. La validation n'est pas déclenchée car l'objet n'existe pas.

La seconde – le JSON est valide, le DTO est assemblé, mais les valeurs sont « mauvaises » selon les règles :

POST http://localhost:8080/api/v1/tasks
Content-Type: application/json

{
  "title": " ",
  "description": "x"
}

Maintenant, la conversion réussit (ce sont des chaînes normales), mais @NotBlank sur title dit : « une chaîne d'espaces ne compte pas ». Et c'est ici que Bean Validation entre en jeu.

Pourquoi est‑ce important ? Parce que plus tard dans le cours, lorsque nous construirons un contrat d'erreur unifié, ces deux cas appartiendront à des catégories d'erreurs différentes. Aujourd'hui, nous n'approfondissons pas encore le format de la réponse, mais le mécanisme doit déjà être clair : des « points de rupture » différents engendrent des erreurs différentes.

Échec de validation et arrêt avant le contrôleur

L'un des effets les plus utiles d'un « contrôleur validant » est que la méthode du contrôleur peut ne pas être appelée du tout si l'entrée n'a pas réussi la vérification. Et ce n'est pas un bug, c'est une fonctionnalité : le contrôleur et le service ne sont pas obligés de traiter une entrée poubelle si l'on s'est mis d'accord à l'avance que l'entrée doit respecter le contrat.

Comment cela se ressent‑il en pratique ? Très simplement. Vous avez une méthode :

package com.example.tasktracker.api.controller;

import com.example.tasktracker.api.dto.request.TaskCreateRequest;
import com.example.tasktracker.api.dto.response.TaskDetailsResponse;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

public class TaskController {

    @PostMapping
    public TaskDetailsResponse create(
            // On dit à Spring MVC : "après la construction du DTO, exécuter Bean Validation"
            @Valid
            // On dit à Spring MVC : "prendre le corps de la requête et le convertir en DTO"
            @RequestBody TaskCreateRequest request
    ) {
        // Si request n'a pas passé la validation, l'exécution n'arrive pas ici du tout
        return null; // Il y aurait un appel au service
    }
}

Si request est invalide, Spring n'entre pas dans create(...). Et on peut le « capturer » même avec un simple log (bien que nous n'abusons pas des logs) : vous ne verrez pas de messages provenant du corps de la méthode car le flux de traitement s'arrête plus tôt.

Que se passe‑t‑il à la place ? Spring forme un scénario négatif : une exception liée à la validation de l'argument de la méthode se produit, puis elle est transformée en réponse HTTP. Aujourd'hui, il suffit de retenir une chose : @Valid n'est pas « vérifier et continuer comme si de rien n'était ». C'est « vérifier et, si c'est mauvais, arrêter la requête ».

5. Exemple complet : DTO et @Valid

Maintenant que la pile est comprise, le flux final paraît assez court. Le DTO de requête porte les contraintes, le contrôleur place @Valid, et le service reçoit un objet déjà vérifié. Ce qui nous importe ici, c'est ce tourniquet d'entrée : le format de la réponse ne change pas le mécanisme de validation.

package com.example.tasktracker.api.controller;

import com.example.tasktracker.api.dto.request.TaskCreateRequest;
import com.example.tasktracker.domain.service.TaskService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/tasks")
public class TaskController {

    private final TaskService taskService;

    public TaskController(TaskService taskService) {
        // Soit un DTO valide parvient au service, soit la requête s'arrête avant
        this.taskService = taskService;
    }

    @PostMapping
    public ResponseEntity<Void> create(
            // Après la construction du DTO, Spring MVC lance Bean Validation
            @Valid @RequestBody TaskCreateRequest request
    ) {
        taskService.create(request);
        return ResponseEntity.status(201).build();
    }
}

Cela suffit déjà pour voir l'enchaînement des couches : le JSON est d'abord transformé en TaskCreateRequest, puis vérifié selon les contraintes, et seulement ensuite le contrôle atteint taskService.create(...). Si le DTO n'est pas construit ou ne réussit pas la vérification, la méthode du contrôleur n'est pas appelée.

6. Erreurs typiques lors de la validation

Erreur n°1 : les annotations sont présentes sur le DTO, mais la validation « semble ne pas fonctionner ».
Le plus souvent, la cause est banale : soit le spring-boot-starter-validation n'est pas déclaré, et l'application n'a pas de fournisseur valide, soit on a oublié de placer @Valid sur l'argument du contrôleur. Le DTO tout seul ne commence pas à être vérifié « par télépathie » : il a besoin qu'une vérification soit lancée au bon endroit dans le pipeline.

Erreur n°2 : s'attendre à ce que Bean Validation intercepte un JSON malformé.
Si le JSON est syntaxiquement cassé ou que les types ne sont pas lisibles, on n'arrive pas jusqu'au DTO. C'est le travail de l'étape HttpMessageConverter, pas du validateur. D'où une confusion typique : le développeur envoie un JSON tordu, reçoit une erreur, voit le statut 400 et pense « c'est mon @NotBlank qui a agi ». Non, c'est une autre classe de problèmes et un autre point d'arrêt.

Erreur n°3 : les mauvais imports (javax.validation.* au lieu de jakarta.validation.*).
Dans la ligne actuelle de Spring Boot et Spring Framework, le monde vit déjà sous jakarta.*. Si vous utilisez accidentellement de vieux imports issus des suggestions de l'IDE ou de vieux articles, vous pouvez obtenir des effets étranges – allant de problèmes de compilation à la situation où « l'annotation semble être là, mais pas la bonne ». Dans un projet d'apprentissage, il vaut mieux s'habituer tout de suite aux bons paquets.

Erreur n°4 : essayer de cacher une logique métier complexe dans les contraintes.
Il y a une tentation : « et si on vérifie tout avec une expression régulière » ou « et si on interdit n'importe quoi avec @Pattern ». Dans ce chapitre, nous apprenons la validation structurelle de base (obligation, longueurs, plages, format simple). Dès qu'une règle nécessite de connaître l'état de l'application (par exemple, « on ne peut pas créer une tâche avec un title qui existe déjà » ou « on ne peut pas modifier une tâche archivée »), ce n'est plus le domaine des contraintes de base sur le DTO.

Erreur n°5 : vérification manuelle et vérification automatique simultanées.
Parfois, un développeur ajoute @Valid mais conserve dans le service une série de if (...) throw ... sur les mêmes champs. En conséquence, la règle vit à deux endroits et tôt ou tard les limites divergent : le DTO dit max 120, le service vérifie max 100, le client pleure, vous aussi. Dans la couche de validation de base, il est préférable de choisir une source de vérité principale pour les règles structurelles : le DTO de requête.

1
Mission
Spring REST & MVC, niveau 15, leçon 1
Bloqué
Démonstration de `Validator` via un endpoint dédié
Démonstration de `Validator` via un endpoint dédié
1
Mission
Spring REST & MVC, niveau 15, leçon 1
Bloqué
Différence entre l'erreur de lecture du body et l'erreur de validation Bean
Différence entre l'erreur de lecture du body et l'erreur de validation Bean
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION