CodeGym /Cours /Spring REST & MVC /Binding des enum, des nombres et des dates

Binding des enum, des nombres et des dates

Spring REST & MVC
Niveau 6, Leçon 3
Disponible

1. Le rôle du binding dans Spring MVC

Si avant vous pensiez que @RequestParam int page était une magie du genre « Spring sait lire dans les pensées », alors nous allons maintenant décomposer cette magie en détails. L’idée clé est simple : HTTP n’apporte pas à votre application des int, LocalDate ou TaskStatus. Il apporte des caractères. En fait, une simple chaîne. Et ensuite Spring essaie de comprendre en quoi la transformer.

Imaginez une requête typique vers notre liste de tâches :

GET /api/v1/tasks?page=1&size=20&status=IN_PROGRESS&dueBefore=2026-03-21

Du point de vue HTTP, ce n’est qu’un ensemble de paires clé=valeur, où les valeurs sont du texte. Même 1 et 20 ne sont pas des nombres, mais une séquence de caractères "1" et "20". Même la date — c’est la chaîne "2026-03-21". Même le statut — la chaîne "IN_PROGRESS".

Et c’est ici qu’il est important : le type de l’argument dans la méthode du contrôleur fait partie du contrat. Dès que vous écrivez TaskStatus status, vous dites au client : « N’envoie pas n’importe quoi, mais une des valeurs de statut autorisées ». Si vous écrivez LocalDate dueBefore, vous dites : « La date doit être dans le format attendu, sinon la requête n’est pas considérée comme correcte ».

Inhabituel ? Oui. Utile ? Aussi oui. Parce que cela rend l’API prévisible : nous ne « devinons » pas ce que le client avait en tête, mais nous disons honnêtement et tôt « oui/non ».

2. Où se fait la conversion des paramètres

Voici un point important souvent négligé : la conversion se fait avant que votre méthode de contrôleur ne commence à s’exécuter. Autrement dit, vous pouvez avoir parfaitement écrit un service, l’avoir couvert de logique, mais si le client envoie page=chat, votre service ne verra même pas cette requête. Spring l’arrête à l’entrée.

Pour bien intégrer cela, il est pratique d’imaginer un mini-pipeline de traitement des paramètres ainsi :

flowchart TD
    A["Requête HTTP"] --> B["Spring extrait les valeurs brutes (raw) du path/query/header/cookie"]
    B --> C["Conversion de type / Formatting ConversionService"]
    C -->|succès| D["Appel de la méthode du contrôleur avec des arguments typés"]
    C -->|erreur| E["Erreur de conversion : la méthode du contrôleur n’est PAS appelée"]

Cette même chaîne fonctionnera aussi plus tard lorsque, au lieu d’un ensemble de paramètres individuels, nous rassemblerons la chaîne de requête en un seul objet critère : la forme de l’argument change, mais pas la mécanique du binding.

Pour le dire plus « en version avancée », Spring MVC pour les paramètres provenant du path/query/header/cookie utilise le mécanisme de type conversion basé sur le ConversionService et les formateurs. Ce n’est pas la même chose que la conversion du body JSON — là ce sera HttpMessageConverter, mais c’est un sujet pour un autre jour. Ici, il s’agit bien de valeurs simples issues de la requête, qui arrivent par défaut sous forme de texte.

Pourquoi est-ce important pour vous en tant que développeur d’API ? Parce que vous cessez de percevoir le contrôleur comme un point de départ. Le point de départ — c’est la frontière de l’API, et le contrôleur fait déjà partie de la requête traitée. Autrement dit, « quelque chose s’est mal passé » peut arriver avant même votre taskService.findTasks(...).

3. Binding d’un enum via TaskStatus

L’enum est l’un des moyens les plus agréables de rendre un contrat honnête. Il fonctionne comme un tourniquet de métro : sans ticket, c’est-à-dire sans valeur valide, on ne passe pas. Et ce n’est pas de la cruauté, mais un souci pour votre futur vous et vos clients : moins il y a de valeurs « au hasard », moins il y a de surprises.

Commençons par le fait que dans le domaine nous avons déjà un ensemble fixe de statuts. Dans le projet, c’est TaskStatus. Montrons explicitement l’enum :

package com.example.tasktracker.domain.model;

/**
 * Statut de la tâche.
 * Important : par défaut lors du binding, Spring attend le nom exact de la constante (la casse compte).
 */
public enum TaskStatus {
    TODO,         // tâche créée mais pas encore en cours
    IN_PROGRESS,  // tâche en cours
    BLOCKED,      // tâche bloquée par des circonstances externes
    DONE,         // tâche terminée
    ARCHIVED      // tâche « retirée des yeux », mais pas supprimée
}

Maintenant, nous voulons que le client puisse appeler la liste des tâches et, s’il le souhaite, filtrer par statut. Au niveau du contrôleur, cela semble très naturel : le statut est un paramètre de requête, et il est facultatif.

import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@GetMapping("/api/v1/tasks")
public Object findTasks(
        // Spring convertira lui-même la chaîne de la query (?status=IN_PROGRESS) en enum TaskStatus
        // Si le paramètre n’est pas envoyé — sera null (on n’applique pas de filtre)
        @RequestParam(required = false) TaskStatus status
) {
    return taskService.findByStatus(status);
}

Remarquez la beauté du service : il reçoit soit null (pas de filtre), soit un véritable enum, et non une chaîne qu’il faudrait comparer manuellement avec "TODO" en espérant nulle part une faute de frappe.

Pour tester manuellement, on peut ajouter une requête .http ou faire la même chose dans Postman :

### Filtrer les tâches par statut
GET http://localhost:8080/api/v1/tasks?status=IN_PROGRESS
Accept: application/json

Maintenant, parlons d’une nuance importante que tout débutant rencontrera inévitablement. Le binding d’enum est par défaut sensible à la casse et attend le nom exact de la constante. Ainsi, cela fonctionnera :

GET http://localhost:8080/api/v1/tasks?status=TODO

Mais ceci, très probablement, échouera avant même le contrôleur :

GET http://localhost:8080/api/v1/tasks?status=todo

Spring ne devinera pas ce que vous vouliez dire. Il dira honnêtement : « Impossible de convertir la chaîne todo en TaskStatus ». C’est un contrat strict. Dans le développement commercial, c’est généralement normal : vous documentez les valeurs autorisées et les clients les respectent. Si vous voulez accepter à la fois todo, TODO et ToDo, c’est une décision distincte concernant une saisie tolérante (tolerant input), et elle nécessite une approche réfléchie.

4. Binding des nombres : page et size

Avec les nombres, tout semble simple jusqu’à ce que quelqu’un envoie size=beaucoup. Et là, vous êtes soudainement ravi que Spring ait arrêté la requête à la frontière, au lieu de laisser ce « beaucoup » atteindre le service. Les paramètres numériques sont un classique des endpoints de liste, et il est pratique de les rendre fortement typés.

À ce stade, nous n’implémentons pas encore une vraie pagination, mais le contrat peut déjà être rendu clair. Par exemple, convenons que par défaut page = 0, size = 20 :

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@GetMapping("/api/v1/tasks")
public Object findTasks(
        // defaultValue rend le paramètre « comme toujours présent » et définit un comportement clair
        @RequestParam(defaultValue = "0") int page,
        // Si on envoie size=beaucoup — la conversion en int échoue avant l’entrée dans la méthode du contrôleur
        @RequestParam(defaultValue = "20") int size
) {
    return taskService.findPage(page, size);
}

Il y a ici deux effets utiles immédiats.

Premièrement, defaultValue rend le comportement déterministe : si le client n’a pas envoyé le paramètre, vous savez exactement ce qui se passera. Vous n’avez pas à parsemer le code de « si null — alors 0 ».

Deuxièmement, le type int (ou Integer) force Spring à effectuer une conversion. Si le client envoie des déchets, la requêtte n’atteindra pas le service.

Testons manuellement :

### Première page, taille 20
GET http://localhost:8080/api/v1/tasks?page=0&size=20
Accept: application/json

Et maintenant un contre-exemple :

### Erreur : page n’est pas un nombre
GET http://localhost:8080/api/v1/tasks?page=chat&size=20
Accept: application/json

Dans ce cas, la conversion de "chat" → int est impossible, et Spring bloque la requête avant même que vous ayez le temps de dire taskService.

Primitives et wrappers : int et Integer

Il y a ici un point pratique. Si le paramètre peut être facultatif, alors Integer est parfois plus honnête car il peut être null. Mais si vous voulez de toute façon des valeurs par défaut, defaultValue + int est généralement l’option la plus simple et la plus propre.

Comment vous écrivez l’argument Peut-il être absent ? Que se passe-t-il s’il est absent Quand c’est pratique
int page + defaultValue oui la valeur par défaut est utilisée pour page/size presque toujours
Integer page + required=false oui sera null quand vous voulez distinguer explicitement « non envoyé »
int page + required=false sans default formellement oui, mais le sens est flou facile d’avoir des attentes étranges il vaut mieux éviter et ne pas jouer à la loterie

Si vous voulez, au début du cours, minimiser les surprises pour votre futur vous, choisissez defaultValue pour les nombres techniques. C’est généralement plus simple à lire et à tester.

5. Binding des dates : LocalDate et format ISO

Les dates dans une API sont une source de douleur éternelle, car les gens aiment les écrire comme ils ont l’habitude : 21.03.2026, 03/21/2026, 21-03-26 et encore dix variantes « comme dans notre comptabilité ». Mais une API est un contrat, pas une dissertation libre. Nous choisissons donc un format et nous y tenons. Dans Spring MVC, pour une date sans heure, le type presque idéal est LocalDate.

Dans Task Tracker API, une tâche a une date limite dueDate, et pour filtrer la liste, il est logique d’avoir des paramètres comme dueBefore et dueAfter. C’est une date sans heure, donc LocalDate convient parfaitement : pas de fuseau horaire, pas de « pourquoi hier est devenu aujourd’hui ».

Exemple de signature :

import java.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@GetMapping("/api/v1/tasks")
public Object findTasks(
        // Paramètre facultatif : si dueBefore n’est pas envoyé — sera null
        @RequestParam(required = false)
        // On fixe explicitement le format : on attend une date ISO AAAA-MM-JJ (pas « comme on a l’habitude »)
        @DateTimeFormat(iso = ISO.DATE) LocalDate dueBefore
) {
    return taskService.findByDueBefore(dueBefore);
}

Ici, @DateTimeFormat(iso = ISO.DATE) est un moyen de rendre le contrat plus explicite. Même si par défaut vous avez déjà le format ISO, l’annotation agit comme un panneau de signalisation : le lecteur du code et vous-même dans un mois comprendrez immédiatement ce qui est attendu : AAAA-MM-JJ.

Requête pour tester :

### Tâches dont la date limite est antérieure à une date spécifique
GET http://localhost:8080/api/v1/tasks?dueBefore=2026-03-21
Accept: application/json

Et maintenant, ce que beaucoup font par habitude et obtiennent une erreur de conversion :

### Erreur : format de date non ISO
GET http://localhost:8080/api/v1/tasks?dueBefore=21.03.2026
Accept: application/json

Spring dira honnêtement : « Impossible d’interpréter la date ». Et c’est normal : vous n’êtes pas obligé de supporter tous les formats du monde. Vous êtes obligé de supporter celui que vous avez décrit dans le contrat.

Paramètres spring.mvc.format.*

Parce que le format des dates et heures dans Spring MVC est effectivement lié aux paramètres de format. On peut les définir globalement via des propriétés de la forme spring.mvc.format.* dans application.yml. Mais une remarque importante : aujourd’hui nous ne configurons pas la conversion globale, nous comprenons seulement qu’elle existe et qu’on ne peut pas laisser le format « au hasard ».

Si vous avez juste besoin de comprendre « où ça vit », un exemple mental suffit :

spring:
  mvc:
    format:
      date: yyyy-MM-dd

Dans un projet pédagogique, il est généralement plus avantageux de s’en tenir aux formats ISO, car ils sont lisibles aussi bien par les humains que par les machines et ne dépendent pas de la locale de la machine du développeur.

6. Erreurs de conversion et signature

Maintenant, on a très envie de demander : « Ok, que se passe-t-il si la valeur est incorrecte ? » Et c’est une bonne question — mais ce qui importe davantage, c’est quand cela se produit. L’erreur de conversion survient avant votre logique métier. Autrement dit, vous pouvez placer un point d’arrêt dans le service, envoyer la requête, voir une erreur dans la réponse — et le point d’arrêt ne se déclenchera pas. Non pas parce que Spring est cassé, mais parce qu’il n’est pas obligé d’appeler votre méthode avec des types incorrects.

Pour ressentir cela, il est parfois utile, uniquement pour soi et temporairement, de faire un diagnostic primitif : par exemple, afficher quelque chose dans le contrôleur. Ce n’est pas une bonne pratique pour la production, mais en tant que marqueur pédagogique, c’est acceptable.

import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@GetMapping("/api/v1/tasks")
public Object findTasks(
        // Ici le paramètre est obligatoire : si le statut n’est pas transmis, on obtient un 400 avant l’exécution de la méthode
        @RequestParam TaskStatus status
) {
    // Diagnostic pédagogique : vous ne verrez la sortie que si le binding réussit
    System.out.println("Controller method invoked");
    return taskService.findByStatus(status);
}

Si vous envoyez status=TODO, la ligne apparaîtra dans la console. Si vous envoyez status=todo — la méthode ne commencera même pas à s’exécuter.

Pourquoi c’est bien pour une API ? Parce que la frontière devient nette. Le client a envoyé un paramètre incorrect — la requête n’est pas considérée comme correcte et le serveur ne fait pas semblant de comprendre. C’est la discipline du contrat. Plus tard dans le cours, nous apprendrons à transformer ces situations en un format d’erreur unique et élégant, mais il est déjà important de voir la mécanique : le contrôleur n’est pas le premier point où tout peut mal tourner.

Mini-refactorisation du GET /api/v1/tasks

Maintenant que nous comprenons le binding, nous voulons assembler une petite structure du futur endpoint de liste. Soulignons : nous ne faisons pas encore de filtrage, tri et pagination complets ; nous montrons simplement que la signature du contrôleur peut être typée et claire. C’est comme installer les bonnes prises dans un appartement avant d’acheter les appareils.

Voici un exemple de signature soignée, qui ressemble déjà à un contrat normal :

import java.time.LocalDate;

import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@GetMapping("/api/v1/tasks")
public Object findTasks(
        // Enum : soit null (pas de filtre), soit une des valeurs autorisées de TaskStatus
        @RequestParam(required = false) TaskStatus status,
        // Pagination technique : valeurs par défaut stables
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        // Dates : on fixe le format ISO et on autorise l’absence du paramètre
        @RequestParam(required = false) @DateTimeFormat(iso = ISO.DATE) LocalDate dueBefore,
        @RequestParam(required = false) @DateTimeFormat(iso = ISO.DATE) LocalDate dueAfter
) {
    return taskService.findTasks(status, page, size, dueBefore, dueAfter);
}

Et voici ce qui est agréable dans le service : il ne pense plus en termes de chaînes. Il pense en types porteurs de sens :

import java.time.LocalDate;

import com.example.tasktracker.domain.model.TaskStatus;

public interface TaskService {
    // Le contrat du service est déjà typé : pas de chaînes HTTP — seulement des types métier/techniques
    Object findTasks(TaskStatus status, int page, int size, LocalDate dueBefore, LocalDate dueAfter);
}

Et c’est probablement le principal résultat pratique de la leçon d’aujourd’hui : un bon contrat API commence par le fait que le type de données est choisi consciemment, et que les erreurs de saisie incorrectes se produisent à la frontière, pas à l’intérieur de l’application.

7. Erreurs typiques lors du binding des enum, nombres et dates

Dans ce domaine, les erreurs ne sont généralement pas compliquées, mais sournoises : vous avez presque tout fait correctement, mais un détail transforme la requête en 400 avant votre code. Voici les râteaux les plus courants dans Spring MVC, surtout quand on débute.

Erreur n°1 : accepter le statut sous forme de String et le traiter manuellement dans le contrôleur.
Un tel code transforme rapidement le contrôleur en mini-parseur : if ("TODO".equals(status)) ... else .... Le contrat devient vague, et les erreurs commencent non pas à la frontière, mais profondément dans l’application. Si la valeur est limitée à un ensemble fixe — l’enum TaskStatus rend cet ensemble explicite, et Spring vérifie lui-même la validité.

Erreur n°2 : s’attendre à ce que l’enum « comprenne tout seul » des valeurs comme in_progress ou InProgress.
Par défaut, le binding d’enum dans Spring repose sur les noms des constantes. Si le contrat dit IN_PROGRESS, c’est exactement cela qu’il faut envoyer. L’envie d’accepter « toutes les variantes d’écriture » semble amicale, mais se termine souvent dans le chaos : les clients commencent à envoyer chacun sa sauce, et vous devez ensuite maintenir un zoo. Mieux vaut un seul format canonique et une documentation correcte.

Erreur n°3 : utiliser un paramètre numérique sans defaultValue et être ensuite surpris du comportement en l’absence de valeur.
Avec des paramètres techniques comme page et size, il est presque toujours plus utile d’avoir des valeurs par défaut claires. Quand vous écrivez @RequestParam(defaultValue="0") int page, le comportement est prévisible. Quand vous laissez faire le hasard, vous créez vous-même une confusion future : est-ce que 0 est « par défaut » ou bien la valeur n’a pas été envoyée ?

Erreur n°4 : écrire les dates dans un format « comme les gens ont l’habitude » et non comme les ordinateurs en ont l’habitude.
21.03.2026 a l’air humain, mais pour un format API ce n’est pas un argument. Dans une API REST, c’est presque toujours l’ISO AAAA-MM-JJ qui gagne. Si vous utilisez LocalDate, donnez tout de suite des exemples de requêtes au format ISO et, si vous le souhaitez, fixez-le via @DateTimeFormat(iso = ISO.DATE) pour éviter l’impression « ça a deviné tout seul ».

Erreur n°5 : essayer d’attraper les erreurs de conversion dans le service et de les « traiter métier ».
L’erreur de conversion de type se produit plus tôt — à l’étape de préparation des arguments de la méthode du contrôleur. Par conséquent, le service ne la verra pas, le contrôleur peut même ne pas démarrer. Si vous vous attendez à « dans le service, je vérifierai que page soit un nombre », vous avez déjà perdu : la requête avec page=chat n’atteindra pas le service. C’est normal ; il faut juste se souvenir où se trouve la frontière.

1
Mission
Spring REST & MVC, niveau 6, leçon 3
Bloqué
Statut de la commande et numéro de page
Statut de la commande et numéro de page
1
Mission
Spring REST & MVC, niveau 6, leçon 3
Bloqué
Filtre des articles par dates de publication
Filtre des articles par dates de publication
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION