Pourquoi valider : sécurité, robustesse et contrat d’API
La validation des entrées consiste à refuser (ou normaliser) toute donnée qui ne respecte pas le contrat attendu par l’API. Elle protège contre les erreurs de type/format, limite les comportements imprévus (champs inconnus, valeurs hors bornes) et rend les erreurs exploitables côté client. On distingue deux niveaux : la validation technique (forme des données) et la validation métier (cohérence fonctionnelle).
1) Validation des paramètres d’URL : path et query
1.1 Paramètres de path : type, format, bornes
Les paramètres de path identifient souvent une ressource. Ils doivent être validés très tôt, avant tout accès aux dépendances (base de données, services externes).
- Type : entier, UUID, slug, etc.
- Format : UUID v4, date ISO-8601, etc.
- Contraintes : min/max, longueur, regex.
Exemples de règles :
GET /users/{userId} userId: UUID v4 obligatoire (regex/parse UUID) -> 400 si invalide (forme de l’URL)1.2 Paramètres de query : optionnels, listes, valeurs autorisées
Les query params sont souvent optionnels. La validation doit couvrir :
- Présence : requis vs optionnel (ex.
fromrequis sitoest fourni). - Type : entier, booléen, date, liste (
ids=1,2,3ouids=1&ids=2). - Ensemble de valeurs : enum (
status=active|disabled). - Bornes :
limitmin/max,page>= 1. - Champs inconnus : stratégie explicite (voir section dédiée).
Exemple de validation de query :
- Écoutez le fichier audio avec l'écran éteint.
- Obtenez un certificat à la fin du programme.
- Plus de 5000 cours à découvrir !
Téléchargez l'application
GET /invoices?status=paid&limit=200&from=2026-01-01status∈ {draft, sent, paid}limitentier, 1..100 (200 => erreur)fromdate ISO-8601
2) Validation du corps JSON : types, formats, contraintes et champs requis
2.1 Règles de base à appliquer systématiquement
- Content-Type : exiger
application/jsonpour les endpoints JSON (sinon 415). - JSON bien formé : erreur de parsing => 400 avec message clair.
- Type des champs : string vs number vs boolean vs object vs array.
- Formats : email, date, UUID, URL (validation stricte).
- Contraintes : min/max, longueur, regex, taille de tableau, unicité dans un tableau.
- Champs requis : présents et non vides selon la règle (attention à la différence entre absent et
null). - Gestion des champs inconnus : refuser ou ignorer, mais de façon cohérente (voir 2.3).
2.2 Exemple concret : création d’un client (POST)
Supposons un endpoint de création :
POST /customersCorps attendu :
{ "email": "a@exemple.com", "firstName": "Ada", "lastName": "Lovelace", "age": 28, "marketingOptIn": true}Règles techniques typiques :
email: requis, string, format email, longueur max 254firstName: requis, string, min 1, max 100lastName: requis, string, min 1, max 100age: optionnel, integer, min 0, max 130marketingOptIn: optionnel, boolean
Exemples d’erreurs techniques :
{ "email": "pas-un-email", "firstName": "", "age": -3, "unknownField": "x"}emailinvalide (format)firstNametrop court (min 1)agehors borne (min 0)unknownFieldnon autorisé (si politique stricte)
2.3 Gestion des champs inconnus : choisir une politique
Deux stratégies principales :
- Politique stricte (recommandée pour un contrat fort) : tout champ non documenté => erreur de validation. Avantage : détecte rapidement les bugs clients, évite la persistance accidentelle de données. Inconvénient : plus sensible aux clients “tolérants”.
- Politique tolérante : ignorer les champs inconnus (ou les stocker dans une structure dédiée). Avantage : compatibilité avec des clients qui envoient plus de données. Inconvénient : risque de masquer des erreurs et d’introduire des comportements implicites.
Bonnes pratiques :
- Être cohérent par endpoint (idéalement par API entière).
- Si vous ignorez, envisagez de renvoyer un avertissement (sans casser le contrat) via un champ optionnel de métadonnées, ou via logs côté serveur.
- Si vous refusez, renvoyer une erreur par champ avec un code stable (voir section 4).
3) Validation technique vs validation métier
3.1 Validation technique (forme)
Elle vérifie que la requête est “parsable” et conforme au schéma : types, formats, contraintes simples, champs requis, champs inconnus. Elle ne nécessite pas (ou très peu) d’accès à l’état du système.
3.2 Validation métier (cohérence fonctionnelle)
Elle vérifie que la commande est cohérente avec les règles fonctionnelles et l’état courant : unicité, transitions autorisées, dépendances entre champs, limites liées au compte, etc. Exemples :
- Interdire
marketingOptIn=truesiemailest absent (si l’email est requis pour l’opt-in). - Refuser une date de fin antérieure à la date de début.
- Empêcher une mise à jour si la ressource est dans un état “verrouillé”.
- Vérifier l’unicité d’un email (souvent nécessite une requête en base).
3.3 Ordre d’exécution recommandé
- Étape 1 : validation technique (rapide, déterministe) : parsing JSON, types, formats, contraintes, champs requis, champs inconnus.
- Étape 2 : normalisation (si applicable) : trim, mise en forme (ex. email en minuscules) sans modifier le sens.
- Étape 3 : validation métier : règles dépendantes de l’état, vérifications d’unicité, transitions, cohérence inter-champs.
- Étape 4 : exécution : persistance, appels externes.
Raison : éviter des accès coûteux (DB) si la requête est déjà invalide techniquement, et produire des erreurs plus claires (un “email invalide” est plus utile qu’un “unicité impossible”).
4) Concevoir des messages de validation exploitables
4.1 Principes : lisible, stable, actionnable
- Liste d’erreurs par champ : permettre au client d’afficher plusieurs erreurs en une fois.
- Codes d’erreur stables : ne pas dépendre du texte (qui peut changer/traduire). Ex.
invalid_format,required,too_short,out_of_range,unknown_field,business_rule_violation. - Chemin du champ : utiliser une notation stable (ex. JSON Pointer
/address/postalCodeou dot-notationaddress.postalCode). - Contexte : inclure
expected,min,max,patternquand utile. - Ne pas fuiter d’informations sensibles : éviter de révéler des détails internes (requêtes SQL, existence d’un compte si cela pose problème).
4.2 Format de réponse recommandé
Un format courant est :
error: catégorie globalemessage: résumé humainviolations: liste structurée
{ "error": "validation_failed", "message": "Some fields are invalid.", "violations": [ { "field": "email", "code": "invalid_format", "message": "Email must be a valid address.", "meta": { "format": "email" } }, { "field": "age", "code": "out_of_range", "message": "Age must be between 0 and 130.", "meta": { "min": 0, "max": 130 } } ]}4.3 400 vs 422 : quand utiliser lequel
Deux approches existent ; l’important est d’être cohérent :
- 400 Bad Request : requête mal formée ou invalide au sens du contrat (JSON invalide, types/format/contraintes, champs inconnus). Beaucoup d’APIs utilisent 400 pour toute validation d’entrée.
- 422 Unprocessable Entity : requête bien formée techniquement, mais impossible à traiter pour des raisons sémantiques (souvent validation métier). Exemple : transition d’état interdite, incohérence inter-champs, règle fonctionnelle violée.
Recommandation pratique :
- Utiliser 400 pour la validation technique.
- Utiliser 422 pour la validation métier (cohérence fonctionnelle) lorsque la requête est syntaxiquement et structurellement correcte.
4.4 Exemples de réponses 400 et 422
Exemple 400 (JSON invalide ou champ inconnu) :
HTTP/1.1 400 Bad RequestContent-Type: application/json{ "error": "validation_failed", "message": "Request body is invalid.", "violations": [ { "field": "unknownField", "code": "unknown_field", "message": "Field is not allowed." } ]}Exemple 422 (règle métier) :
HTTP/1.1 422 Unprocessable EntityContent-Type: application/json{ "error": "business_rule_violation", "message": "Request is semantically invalid.", "violations": [ { "field": "email", "code": "already_in_use", "message": "Email is already used.", "meta": { "constraint": "customers_email_unique" } } ]}5) Stratégie de validation pour PATCH (mises à jour partielles)
5.1 Problème : “requis” ne veut pas dire la même chose en PATCH
En PATCH, le client envoie un sous-ensemble de champs. Les règles changent :
- Un champ requis à la création n’est pas forcément requis en PATCH.
- Mais si un champ est présent dans le PATCH, il doit respecter ses contraintes (type, format, bornes).
5.2 Approche étape par étape (PATCH JSON « merge »)
Si vous utilisez un PATCH de type “merge” (corps JSON partiel), une approche robuste est :
- Étape 1 : valider le JSON partiel : types/formats/contraintes sur les champs présents uniquement, et champs inconnus selon votre politique.
- Étape 2 : charger l’état courant de la ressource.
- Étape 3 : appliquer le patch (fusion) en mémoire.
- Étape 4 : valider le résultat final : s’assurer que l’objet complet respecte les invariants (techniques et métier). Exemple : après patch, un champ requis ne doit pas devenir vide si la règle l’interdit.
- Étape 5 : persister.
Cette double validation évite un piège classique : un patch qui semble valide isolément mais rend l’objet final incohérent.
5.3 Gestion des valeurs null : absent vs null
Décidez explicitement de la sémantique :
- Champ absent : ne pas modifier.
- Champ présent avec valeur non-null : mettre à jour.
- Champ présent avec null : deux options cohérentes :
- Option A (null = effacer) : autoriser
nulluniquement pour les champs “nullable”. Pour les champs non-nullables, renvoyer une violationnot_nullable. - Option B (null interdit) : exiger que l’effacement passe par une action explicite (ex. endpoint dédié, ou JSON Patch avec opération remove). Dans ce cas,
null=> erreurinvalid_typeounull_not_allowed.
Exemple (Option A) :
PATCH /customers/{id}{ "age": null}Si age est nullable : OK (effacement). Si non : 422 ou 400 selon si vous considérez cela technique (schéma) ou métier (règle).
5.4 PATCH et champs inconnus
En PATCH, refuser les champs inconnus est souvent préférable : un client qui tente de mettre à jour un champ inexistant doit être alerté. Sinon, il peut croire que la mise à jour a été prise en compte.
6) Compatibilité lors d’ajouts de champs : éviter de casser les clients
6.1 Ajout de champs côté API : règles de compatibilité
- Ajouter un champ optionnel est généralement rétrocompatible.
- Ajouter un champ requis casse les clients existants (ils ne l’enverront pas). Préférer une phase transitoire : d’abord optionnel, puis requis dans une nouvelle version/contrat.
- Ajouter un champ avec valeur par défaut : utile pour migration progressive, mais documenter la valeur par défaut et la logique associée.
6.2 Politique “champs inconnus” et compatibilité
La compatibilité dépend aussi de votre politique :
- Si votre API refuse les champs inconnus, un client qui envoie un champ “futur” échouera sur un serveur “ancien”. C’est acceptable si vous contrôlez les clients ou versionnez strictement.
- Si votre API ignore les champs inconnus, un client peut envoyer des champs supplémentaires sans casser les serveurs plus anciens, mais vous perdez la détection d’erreurs.
Compromis fréquent : stricte sur les endpoints d’écriture (POST/PUT/PATCH), plus tolérante sur certains endpoints de lecture si nécessaire, tout en restant cohérent et documenté.
7) Check-list d’implémentation (pratique)
7.1 À l’entrée de chaque endpoint
- Valider
Content-Typeet parser le JSON (si applicable). - Valider path params (type/format) avant toute logique.
- Valider query params : types, bornes, enums, dépendances simples.
- Valider le body : schéma, champs requis, contraintes, champs inconnus.
7.2 Avant d’exécuter la commande
- Normaliser (trim, minuscules) si vous le faites.
- Valider métier : cohérence inter-champs, état courant, unicité, transitions.
- Construire une réponse d’erreurs structurée :
violations[]avecfield+code+message+meta.
7.3 Pour PATCH
- Valider uniquement les champs présents (forme).
- Appliquer le patch sur l’état courant.
- Revalider l’objet final (invariants) et les règles métier.
- Définir la sémantique de
null(effacement vs interdit) et s’y tenir.