Aller au contenu
English

Modèles de requête et de réponse

  • Leçon 5
  • 40 min
  • Module 2 : Créer un petit service FastAPI

Ce que vous saurez faire

Ajoutez le point de terminaison qui fait le vrai travail : POST /classify lit un billet de soutien en JSON et renvoie sa catégorie et sa priorité. Les modèles typés vérifient chaque billet avant que votre code le reçoive. Un objet manquant produit donc un code 422 clair, plutôt qu’un plantage.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 40 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

Testé avec Python 3.14.6, FastAPI 0.142.2 et Pydantic 2.13 sur macOS.

Dans le cours Python, find_problems vérifiait chaque billet à la main : l’objet est-il présent? La priorité est-elle un nombre de 1 à 3? Une API reçoit du JSON de programmes que vous ne contrôlez pas. Elle doit donc faire les mêmes vérifications, pour chaque champ de chaque requête.

FastAPI utilise Pydantic pour cela. Un modèle Pydantic est une classe qui énumère les champs de données, avec un type et des limites pour chacun. Il ressemble à une classe de données :

from pydantic import BaseModel, Field
class TicketIn(BaseModel):
subject: str = Field(min_length=1, max_length=120)
body: str = Field(default="", max_length=5000)
Partie Signification
class TicketIn(BaseModel) Un modèle pour l’entrée du point de terminaison : un billet.
subject: str Le champ doit être une chaîne de caractères. Il n’a aucune valeur par défaut, donc il est obligatoire.
Field(min_length=1, max_length=120) Limites : au moins 1 caractère, au plus 120.
body: str = Field(default="", ...) Facultatif. Si le client l’omet, sa valeur est "".

Une fonction de point de terminaison peut avoir un paramètre dont le type est un modèle. FastAPI lit alors le corps JSON et le vérifie selon le modèle, puis appelle votre fonction. Si le corps ne respecte pas le modèle, FastAPI répond 422 et votre fonction ne s’exécute jamais.

La sortie a aussi un modèle. FastAPI lit le type de retour de la fonction. Il l’utilise pour vérifier le résultat et écrire le JSON :

from typing import Literal
Category = Literal["billing", "login", "shipping", "account", "other"]
class Classification(BaseModel):
category: Category
priority: int = Field(ge=1, le=3)
confidence: float = Field(ge=0, le=1)
model_version: str

Literal[...] signifie « une seule de ces valeurs ». ge et le signifient « supérieur ou égal à » et « inférieur ou égal à ». Le modèle de réponse est une promesse à chaque client : la catégorie est l’un des cinq mots possibles. La priorité est toujours 1, 2 ou 3. Si votre code tente de rompre cette promesse, Pydantic l’arrête (voir l’erreur courante ci-dessous).

Exemple détaillé : le point de terminaison de classification

Section intitulée « Exemple détaillé : le point de terminaison de classification »

Pour l’instant, la classification est simple : la première catégorie dont le mot-clé apparaît dans le texte l’emporte. La prochaine leçon la déplace dans son propre module et l’améliore. Voici le fichier main.py complet :

ticket_api/main.py
from typing import Literal
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI(title="Ticket Classifier API")
Category = Literal["billing", "login", "shipping", "account", "other"]
KEYWORDS = {
"billing": ("invoice", "charged", "refund", "payment"),
"login": ("password", "sign in", "log in", "two-factor", "locked out"),
"shipping": ("parcel", "delivery", "courier", "tracking"),
"account": ("account", "company name", "user"),
}
class TicketIn(BaseModel):
subject: str = Field(min_length=1, max_length=120)
body: str = Field(default="", max_length=5000)
class Classification(BaseModel):
category: Category
priority: int = Field(ge=1, le=3)
confidence: float = Field(ge=0, le=1)
model_version: str
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/classify")
def classify(ticket: TicketIn) -> Classification:
text = f"{ticket.subject}\n{ticket.body}".lower()
for category, words in KEYWORDS.items():
if any(word in text for word in words):
return Classification(
category=category, priority=2, confidence=0.7, model_version="keywords-0.1"
)
return Classification(
category="other", priority=3, confidence=0.2, model_version="keywords-0.1"
)

Dans la fonction, ticket est un objet TicketIn, déjà vérifié : ticket.subject est une chaîne de 1 à 120 caractères. La fonction ne fait aucune vérification elle-même. Elle renvoie une Classification, et FastAPI la transforme en JSON.

Amira conserve ses corps de requête dans des fichiers, dans un dossier requests. Elle peut ainsi les envoyer de nouveau facilement. Voici un billet valide :

requests/billing.json
{"subject": "Charged twice for March", "body": "My card was charged two times for the same invoice."}
Terminal (on Windows: curl.exe)
curl -i -X POST http://127.0.0.1:8000/classify -H "Content-Type: application/json" --data @requests/billing.json
Output (first and last line)
HTTP/1.1 200 OK
{"category":"billing","priority":2,"confidence":0.7,"model_version":"keywords-0.1"}

Puis trois billets invalides. Elle prédit le code d’état avant chaque envoi :

Fichier Corps Réponse
empty-subject.json {"subject": ""} 422 : "type":"string_too_short", "loc":["body","subject"], "msg":"String should have at least 1 character" (la chaîne doit contenir au moins 1 caractère)
no-subject.json {"body": "Hello"} 422 : "type":"missing", "loc":["body","subject"], "msg":"Field required" (champ obligatoire)
wrong-types.json {"subject": 42, "body": ["a"]} 422, avec deux erreurs : subject et body ne sont pas des chaînes de caractères

Voici le corps complet pour l’objet vide :

Response body (422)
{"detail":[{"type":"string_too_short","loc":["body","subject"],"msg":"String should have at least 1 character","input":"","ctx":{"min_length":1}}]}

loc indique l’emplacement du problème : dans le body (corps), le champ subject (objet). FastAPI signale tous les problèmes à la fois, comme find_problems dans le cours Python. Dans le module 3, vous remplacerez ce format par un format plus court, identique pour toutes les erreurs.

Exercice guidé : créer et appeler le point de terminaison

Section intitulée « Exercice guidé : créer et appeler le point de terminaison »
  1. Remplacez ticket_api/main.py par l’exemple détaillé et enregistrez-le. Le serveur de développement redémarre.

  2. Créez le dossier requests avec les quatre fichiers du tableau : billing.json, empty-subject.json, no-subject.json et wrong-types.json.

  3. Envoyez chaque fichier avec curl, comme dans l’exemple détaillé. Avant chaque envoi, notez le code d’état attendu.

    billing.json donne 200. Les trois autres donnent 422, avec les valeurs de loc et de msg (message) du tableau.

  4. Envoyez un corps avec un champ supplémentaire : {"subject": "Refund", "colour": "red"}.

    Il donne 200. Pydantic ignore les champs absents du modèle. Le module 3 fera en sorte que l’API les rejette. Une faute de frappe dans un nom de champ ne sera donc plus ignorée silencieusement.

  5. Envoyez un objet de 121 caractères. (Créez le fichier avec Python : python -c "import json; print(json.dumps({'subject': 'x' * 121}))" > requests/long.json.)

    Il donne 422 avec string_too_long (chaîne trop longue). Essayez ensuite exactement 120 caractères : 200. Vous avez testé les deux côtés de la limite.

Le service de soutien envoie le canal de chaque billet : email (courriel), phone (téléphone) ou chat (clavardage). Ajoutez à TicketIn un champ facultatif channel, avec la valeur par défaut "email", qui accepte uniquement ces trois valeurs. Ajoutez ensuite le canal à la réponse, sous forme de champ channel dans Classification. Le client voit ainsi quel canal l’API a utilisé.

Testez quatre corps : avec "channel": "chat", sans canal, avec "channel": "fax" et avec "channel": "Chat". Prédisez d’abord chaque résultat.

Afficher une réponse
Channel = Literal["email", "phone", "chat"]
class TicketIn(BaseModel):
subject: str = Field(min_length=1, max_length=120)
body: str = Field(default="", max_length=5000)
channel: Channel = "email"
class Classification(BaseModel):
category: Category
priority: int = Field(ge=1, le=3)
confidence: float = Field(ge=0, le=1)
model_version: str
channel: Channel

Dans classify, passez channel=ticket.channel aux deux appels de Classification(...). "chat" donne 200 avec "channel":"chat". Sans canal, vous obtenez "channel":"email". "fax" et "Chat" donnent tous deux 422 avec literal_error : un Literal compare les valeurs exactement, donc les majuscules comptent. Retirez de nouveau le champ avant la prochaine leçon. Le reste du cours ne l’utilise pas.

Erreur courante

Erreur 500 Internal Server Error causée par votre propre résultat

Symptôme : une requête qui semble valide reçoit 500 Internal Server Error (erreur interne du serveur). Son corps est Internal Server Error, et non du JSON. Le terminal du serveur affiche une longue trace d’appels qui se termine ainsi :

pydantic_core._pydantic_core.ValidationError: 1 validation error for Classification
priority
Input should be greater than or equal to 1 [type=greater_than_equal, input_value=0, input_type=int]

Cause : votre code a créé une Classification qui ne respecte pas les limites du modèle, ici priority=0. Pydantic vérifie les valeurs lorsque vous créez le modèle et les refuse. C’est un défaut du code, pas un problème de requête, donc le client reçoit 500. Le modèle a fait son travail : une priorité incorrecte n’a jamais atteint un client.

Correction :

  1. Lisez les dernières lignes de la trace d’appels : le modèle, le champ, la valeur et la règle.
  2. Trouvez où votre code produit cette valeur et corrigez-le.
  3. N’assouplissez pas le modèle pour faire disparaître l’erreur. La limite fait partie de la promesse à vos clients.

Vérifiez votre compréhension

Un client envoie {« subject » : « »}. La fonction classify s'exécute-t-elle? Que reçoit le client?

Afficher la réponse

La fonction ne s’exécute pas. FastAPI vérifie d’abord le corps selon TicketIn et constate que subject contient moins de 1 caractère. Il répond 422 Unprocessable Content (contenu impossible à traiter) avec une erreur dont loc vaut ["body", "subject"]. Son type est string_too_short (chaîne trop courte). La fonction ne reçoit que des billets valides.

Vérifiez votre compréhension

Pourquoi le point de terminaison renvoie-t-il un modèle Classification plutôt qu'un simple dictionnaire?

Afficher la réponse

Le modèle est une promesse vérifiée. Pydantic refuse un résultat qui la rompt, comme une catégorie inconnue ou une priorité de 0. Le client ne reçoit donc jamais de valeur incorrecte. FastAPI utilise aussi le modèle pour documenter la réponse. Avec un dictionnaire, n’importe quelle valeur pourrait atteindre le client.

Récapitulatif

  • Un modèle Pydantic énumère les champs des données, avec leurs types, leurs limites et leurs valeurs par défaut.
  • Un paramètre typé avec un modèle fait lire et vérifier le corps JSON par FastAPI avant l’exécution de votre fonction. Une entrée invalide reçoit 422 avec tous les problèmes.
  • Le type de retour est le modèle de réponse : une promesse vérifiée sur la sortie.
  • Conservez les corps de requête de test dans des fichiers et testez les deux côtés de chaque limite.

Ensuite, vous déplacerez le classificateur hors de main.py et donnerez à chaque partie de l’API son propre module.

Leçon suivante : Séparer les responsabilités