loader-logo

Spring boot – Gestion des erreurs de validation dans l’API RESTful

90% des projets actuels utilisent L’API Restful pour y exposer les services, avec Spring Boot cela devient un jeu d’enfant. Mais qu’en est-il de la validation (le sujet principal de ce poste), des exceptions et du traitement des erreurs (voir la partie 2) lorsque vous traitez avec ce genre de services ? Que pouvons-nous faire lorsque quelque chose va mal ? Et comment Spring pourrait nous aider ?

Ce poste sera l’un des nombreux articles liés au cadre

de Spring qui seront disponibles dans mon blog

personnel (pour très bientôt 😊 )

Ce poste ne couvre pas seulement le contrôle de validation des entrées, mais aussi la façon de retourner une bonne réponse au client quand quelque chose va mal, alors commençons (comme Linus Torvalds a dit : « Les paroles ne valent rien, montrez-moi le code »).

Le traitement de validation est l’une des clés importantes qui donne à votre API une bonne réputation, sachant que chaque fois qu’un client vous enverra une requête, vous la contrôlerez pour vérifier si elle a des erreurs, un mauvais format, des valeurs requises manquantes, certains business pour exister…

Spring boot hors de la boîte de validation :

Spring nous offre une validation simple, c’est-à-dire vérifier que certains champs obligataires sont présents dans la demande via la priorité requise (qui est définie à True par défaut) des annotations @Pathvariable et @Requestparam :

@GetMapping(value = "/foo/{id}")
public String getFoo(@PathVariable(name="id", required= true) Integer id)

 

Ainsi, lorsque l’id n’est pas présent ou n’a pas d’autres valeur que Integer, Spring retournera au client un message contenant « 400 Bad Request »

Cela ne peut être utile que pour les types simples, mais quand il s’agit d’un objet complexe qui est analysé à partir d’un JSON nous devons faire plus que d’utiliser la propriété requise.

Traitement de validation à l’aide de l’annotation ExceptionHandler

ExceptionHandler est utilisé pour gérer les exceptions de certaines classes de Handler et/ou méthodes de Handler. Les méthodes du gestionnaire qui sont annotées avec cette annotation peuvent avoir des paramètres des types suivant dans un ordre arbitraire :

  • Un argument d’exception : déclaré comme une exception générale ou comme une exception plus spécifique.
  • Objets de requête et/ou de réponse (généralement à partir de l’API Servlet).
  • Objets de session
  • … (voir le Java Doc officiel pour plus de détails).

Donc, pour que votre entreprise de validation soit opérationnelle, nous devrions :

Annotez vos DTO (objet de transfert de données entre votre API et les clients), avec vos contraintes de validation en utilisant JSR 303 Bean Validation dans le support, en annotant les champs avec des annotations comme @Notnull, @Email, @Size, @Pattern, @Min, @Man. Ces annotations ont une propriété de message qui vous permet de personnaliser le message de sortie lorsque la validation échoue dans le champ annoté :

@Data
public class Foo{
    
    @NotNull(message = "id is required")
    private Long id;
    
    @NotNull@Email(message = "email is not valid")
    private String email;

    @Pattern(regexp = "^[ABC]$", message = "Must be either A, B or C")
    private String action;

}

 

 

De cette façon, l’objet Foo sera validé sur chaque demande à /Foo Endpoint.

Ajoutez l’annotation @Valid, qui marque une propriété, un paramètre de méthode ou un type de retour de méthode pour la validation en cascade vers le corps de votre requête :

@PutMapping(value = "/foo") 
public String updateFoo(@Valid @RequestBody Foo foo)

 

De cette façon, l’objet Foo sera validé sur chaque demande à /foo endpoint.

Créer une classe ErrorResponse personnalisée qui sera renvoyée au client comme réponse en cas d’échec de validation :

/**
 * This class holds a list of {@code ErrorModel} that describe the error raised on rejected validation
 * @author ROUSSI Abdelghani
 */

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse {
    private List<ErrorModel> errorMessage;

}
/**
 * This class describe the error object model, which is a simple POJO that contains the rejected filedName, rejectedValue
 * and a messageError.
*/
@Data
@NoArgsConstructor    
@AllArgsConstructor
public class ErrorModel{
    private String fieldName;
    private Object rejectedValue;
    private String messageError;
}

 

 

Enfin, créer la méthode qui prendra une MethodArgumentNotValidException  et retournera le message d’erreur :

/**
 * Method that check against {@code @Valid} Objects passed to controller endpoints
 *
 * @param exception
 * @return a {@code ErrorResponse}
 * @see com.aroussi.util.validation.ErrorResponse
 */
@ExceptionHandler(value=MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleException(MethodArgumentNotValidException exception) {

    List<ErrorModel> errorMessages = exception.getBindingResult().getFieldErrors().stream()
            .map(err -> new ErrorModel(err.getField(), err.getRejectedValue(), err.getDefaultMessage()))
            .distinct()
            .collect(Collectors.toList());
    return ErrorResponse.builder().errorMessage(errorMessages).build();
}

 

Notez l’attribut de valeur à l’annotation @Exceptionhandler, qui déclare les exceptions qui seront traitées par la méthode annotée. Si elle est vide, la valeur par défaut des exceptions répertoriées dans la liste d’arguments de la méthode.

Mais que se passe-t-il si le client nous envoie un corps de requête incorrect ? (syntaxe déformée)

Dans ce cas nous aurons une entité non nécessaire 422 avec une réponse serveur comme celle-ci :

{
  "timestamp": "2018-10-15T13:17:45.244+0000",
  "status": 400,
  "error": "Bad Request",
  "message": "JSON parse error: Unexpected end-of-input within/between Object entries; nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected end-of-input within/between Object entries at [Source: (PushbackInputStream); line: 9, column: 201]",
  "path": "/api/foo"
}

 

 

Pour nous en débarrasser nous allons suivre la même stratégie que la précedente (pour les exceptions de validation de MethodArgumentNotValidException), mais cette fois pour les exceptions de HttpMessageNotReadableException :

/**
 * Handle unprocessable json data exception
 * @param msgNotReadable
 * @return a {@code ErrorResponse}
 */
@ExceptionHandler(value=HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public ErrorResponse handleUnprosseasableMsgException(HttpMessageNotReadableException msgNotReadable) {
       // note that we've added new properties (message, status) to our ErrorResponse model return ErrorResponse.builder()
            .message("UNPROCESSABLE INPUT DATA")
            .status(HttpStatus.UNPROCESSABLE_ENTITY.value())
            .build();
}

 

Nous avons plusieurs options pour aller encore plus loin, et les validations de groupe (Globalement) dans une classe annotée avec @Controlleradvice (par défaut, les méthodes dans un @Controlleradvice s’appliquent globalement à tous les contrôleurs), mais je préfère garder les choses simples dans cet article et introduire les fonctionnalités plus avancées dans les prochains articles.

Résumé :

Ce n’est pas la seule façon d’effectuer la validation dans le cadre de Spring, il existe d’autres façons de le faire (en utilisant l’interface Validator par exemple), mais c’est un bon début, même si Spring considère que la validation ne devrait pas être liée au volet Web parce que c’est une préoccupation croisée qui devait être facile à localiser et il devait être possible de brancher n’importe quel validateur disponible. C’est donc ainsi que Spring a mis au point une interface Validator. Envisagez alors d’avoir un regard sur cette façon de valider. Personnellement (IMHO) je pense que cette partie de la documentation (validation à l’aide de l’interface e validation de Spring) manque d’exemple lorsqu’il s’agit de résoudre le code de message d’erreur.

 

Bonne chance !

 

Roussi Abdelghani

Partager sur facebook
Partager sur twitter
Partager sur linkedin
Partager sur pinterest
Partager sur whatsapp

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Sélectionnez votre pays et votre langue​

Maroc

Tunis

Allemagne

Switzerland

France

Global