Contactez-nous

Création de contraintes de validation personnalisées

Apprenez à créer des annotations et des validateurs personnalisés avec Bean Validation (JSR 380) pour implémenter des règles métier spécifiques dans vos applications Spring Boot.

Pourquoi des contraintes personnalisées ?

Les annotations de validation standard fournies par Bean Validation (@NotNull, @Size, @Pattern, @Email, etc.) couvrent un large éventail de cas d'utilisation courants. Cependant, il arrive fréquemment que les règles métier spécifiques à une application nécessitent des validations plus complexes ou plus spécifiques qui ne sont pas couvertes par les contraintes standard.

Par exemple, vous pourriez avoir besoin de vérifier :

  • Qu'un numéro de produit suit un format interne spécifique.
  • Qu'un code postal est valide pour un pays donné.
  • Qu'une valeur est unique dans la base de données (bien que cela soit souvent géré aussi au niveau de la base de données).
  • Qu'un champ respecte une logique métier particulière (par exemple, un numéro de série doit correspondre à un certain algorithme de checksum).
  • Des validations croisées entre plusieurs champs (bien que Bean Validation soit moins idéal pour cela, c'est parfois tenté).

Plutôt que d'implémenter cette logique de validation de manière répétitive dans vos services ou contrôleurs, Bean Validation permet de créer vos propres contraintes de validation personnalisées. Cela maintient l'approche déclarative et réutilisable de la validation, en encapsulant la logique métier spécifique dans des composants dédiés.

Les deux piliers d'une contrainte personnalisée

La création d'une contrainte de validation personnalisée dans Bean Validation repose sur deux éléments principaux :

  1. Une interface d'annotation personnalisée : C'est l'annotation que vous utiliserez pour marquer les champs à valider (par exemple, @ValidProductCode). Cette annotation définit les métadonnées de la contrainte, comme le message d'erreur par défaut, et surtout, elle pointe vers la classe qui contient la logique de validation.
  2. Une classe d'implémentation du validateur : Cette classe implémente l'interface jakarta.validation.ConstraintValidator (ou javax.validation.ConstraintValidator). Elle contient le code Java qui effectue réellement la vérification de la contrainte.

Ces deux composants travaillent ensemble : l'annotation déclare la contrainte sur le bean, et le framework Bean Validation utilise l'information de l'annotation (via @Constraint(validatedBy = ...)) pour trouver et invoquer le validateur approprié.

Etape 1 : Créer l'interface d'annotation

Commençons par créer l'annotation elle-même. Il s'agit d'une interface Java standard annotée avec @interface. Pour qu'elle soit reconnue comme une contrainte Bean Validation, elle doit être décorée par trois méta-annotations essentielles :

  • @Target : Spécifie où l'annotation peut être appliquée (champs, méthodes, paramètres, types, etc.). Pour la validation de champs, on utilise souvent ElementType.FIELD et ElementType.ANNOTATION_TYPE (pour permettre la composition d'annotations).
  • @Retention(RetentionPolicy.RUNTIME) : Indique que l'annotation doit être disponible à l'exécution via la réflexion, ce qui est nécessaire pour que le framework de validation puisse la lire.
  • @Constraint(validatedBy = VotreClasseValidator.class) : C'est le lien crucial. Il indique quelle(s) classe(s) implémente(nt) la logique de validation pour cette annotation.

Il est également recommandé d'ajouter @Documented pour qu'elle apparaisse dans la Javadoc.

L'interface doit aussi définir trois méthodes obligatoires, correspondant aux attributs standard des contraintes Bean Validation :

  • message() : Définit le message d'erreur par défaut si la validation échoue. Doit avoir une valeur par défaut.
  • groups() : Permet de regrouper les contraintes (concept avancé). Doit avoir une valeur par défaut (généralement {} ou {jakarta.validation.groups.Default.class}).
  • payload() : Permet d'attacher des métadonnées supplémentaires à la contrainte (concept avancé). Doit avoir une valeur par défaut (généralement {} ou {jakarta.validation.Payload.class}).

Exemple : Créons une annotation @ValidCourseCode pour valider un code de cours qui doit commencer par "CRS-".

package com.myapp.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Constraint(validatedBy = CourseCodeValidator.class) // Lien vers le validateur
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidCourseCode {

    // Message d'erreur par défaut
    String message() default "Le code de cours doit commencer par CRS-";

    // Groupes de validation (requis)
    Class[] groups() default {};

    // Payload (requis)
    Class[] payload() default {};
    
    // --- Attributs personnalisés (optionnel) ---
    // Vous pourriez ajouter des attributs ici si votre validateur a besoin de paramètres
    // String value() default "CRS-"; // Exemple d'attribut pour passer le préfixe
}

Si vous ajoutez des attributs personnalisés (comme value() dans l'exemple commenté), vous pourrez les récupérer dans le validateur.

Etape 2 : Implémenter le ConstraintValidator

Maintenant, créons la classe `CourseCodeValidator` qui contient la logique de validation. Elle doit implémenter l'interface `ConstraintValidator`, où :

  • `A` est le type de votre annotation personnalisée (`ValidCourseCode` dans notre cas).
  • `T` est le type de l'élément qui sera validé (le type du champ sur lequel l'annotation sera appliquée, par exemple `String`).

Cette interface a deux méthodes à implémenter :

  • initialize(A constraintAnnotation) : Appelée une seule fois par le framework avant toute validation. Vous pouvez l'utiliser pour récupérer les valeurs des attributs personnalisés de votre annotation si vous en avez défini (par exemple, récupérer le préfixe via `constraintAnnotation.value()`).
  • isValid(T value, ConstraintValidatorContext context) : C'est ici que réside la logique de validation principale. Elle reçoit la valeur du champ (`value`) à valider et un contexte (`context`). Elle doit retourner `true` si la valeur est valide, et `false` sinon. Important : La méthode `isValid` doit souvent gérer le cas où `value` est `null`. Généralement, une contrainte personnalisée considère `null` comme valide, laissant le soin à l'annotation `@NotNull` de vérifier la nullité si nécessaire.

Exemple d'implémentation pour `CourseCodeValidator` :

package com.myapp.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class CourseCodeValidator implements ConstraintValidator {

    private String coursePrefix; // Pour stocker un éventuel préfixe venant de l'annotation

    @Override
    public void initialize(ValidCourseCode constraintAnnotation) {
        // Si on avait un attribut 'value()' dans l'annotation pour le préfixe:
        // coursePrefix = constraintAnnotation.value(); 
        // Dans notre cas simple, on le met en dur pour l'exemple
        coursePrefix = "CRS-"; 
    }

    @Override
    public boolean isValid(String courseCode, ConstraintValidatorContext context) {
        // Règle métier : null est considéré comme valide ici (laisser @NotNull faire son travail)
        if (courseCode == null) {
            return true; 
        }
        
        // Règle métier : le code doit commencer par le préfixe défini
        boolean result = courseCode.startsWith(coursePrefix);
        
        // Vous pourriez ajouter une logique plus complexe ici...
        // par exemple, vérifier la partie numérique après le préfixe.
        
        return result;
    }
}

Etape 3 : Utiliser la nouvelle annotation

Une fois l'annotation et son validateur créés, vous pouvez utiliser votre nouvelle annotation exactement comme n'importe quelle contrainte Bean Validation standard, en l'appliquant aux champs appropriés de vos DTOs ou entités :

import com.myapp.validation.ValidCourseCode;
import jakarta.validation.constraints.NotBlank;

public class CourseDto {

    @NotBlank(message = "Le code est requis.")
    @ValidCourseCode // Notre annotation personnalisée !
    private String code;

    @NotBlank(message = "Le titre est requis.")
    private String title;

    // Getters et Setters...
}

Désormais, lorsque vous utiliserez @Valid sur un objet `CourseDto` dans votre contrôleur Spring Boot, le `CourseCodeValidator` sera automatiquement invoqué pour vérifier le champ `code`, en plus des autres validateurs standard comme @NotBlank.

Gestion des messages d'erreur

Le message d'erreur affiché en cas d'échec de la validation est celui défini par l'attribut `message()` de votre annotation personnalisée ("Le code de cours doit commencer par CRS-" dans notre exemple). Comme pour les contraintes standard, vous pouvez externaliser et personnaliser ces messages via le fichier `ValidationMessages.properties` (placé dans `src/main/resources`).

La clé pour surcharger le message d'une annotation personnalisée est généralement son nom de classe complet. Pour notre exemple :

# Dans src/main/resources/ValidationMessages.properties
com.myapp.validation.ValidCourseCode.message=Code de cours invalide. Il doit commencer par CRS-.

Cela permet de gérer l'internationalisation (i18n) de vos messages d'erreur personnalisés de la même manière que pour les messages standard.

Conclusion : flexibilité et réutilisabilité

La création de contraintes de validation personnalisées est une fonctionnalité puissante de Bean Validation, parfaitement intégrée à Spring Boot. Elle vous permet d'encapsuler des règles métier complexes ou spécifiques dans des composants réutilisables et déclaratifs.

En définissant une annotation et un validateur associé, vous pouvez appliquer ces règles de manière cohérente dans toute votre application, améliorant ainsi la robustesse, la maintenabilité et la clarté de votre code de validation. C'est un outil essentiel pour garantir la qualité et l'intégrité des données conformément aux exigences spécifiques de votre domaine métier.