Aller au contenu
English

Une documentation utilisable

  • Leçon 11
  • 45 min
  • Module 4 : Documenter l'API avec OpenAPI et Swagger UI

Ce que vous saurez faire

Faites en sorte que /docs réponde aux questions d’une personne inconnue sans qu’elle ait à vous les poser. La documentation doit expliquer le rôle de chaque point de terminaison et le sens de chaque champ. Elle doit montrer une vraie requête et chaque erreur possible. Un développeur qui peut utiliser votre API avec la documentation seule n’a pas besoin d’une rencontre avec vous.

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

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

Testé avec FastAPI 0.142.2 et Pydantic 2.13 sur macOS.

Tout dans Swagger UI vient de votre code. Pour améliorer la documentation, ajoutez l’information au bon endroit :

Dans Swagger UI Provient de Où
Titre, version et texte sous ceux-ci title, version et description de FastAPI(...) main.py
Noms des sections et leurs descriptions tags sur les routes et openapi_tags dans FastAPI(...) routes.py, main.py
Ligne à côté du chemin summary= sur la route routes.py
Texte affiché quand vous ouvrez un point de terminaison description= sur la route routes.py
Chaque code d’état documenté Type de retour et responses= sur la route routes.py
Descriptions et limites des champs Field(description=..., ...) models.py
Corps donné en exemple json_schema_extra={"examples": [...]} sur le modèle models.py

Écrivez pour un développeur qui n’a jamais vu votre code. Expliquez ce qu’une valeur signifie, pas seulement son type. « 1 est le plus urgent, 3 le moins urgent » est utile; « la priorité » ne l’est pas.

Amira ajoute une chaîne de documentation, des descriptions de champs et un exemple à chaque modèle. Voici le nouveau models.py :

ticket_api/models.py
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
Category = Literal["billing", "login", "shipping", "account", "other"]
class TicketIn(BaseModel):
"""A support ticket to classify."""
model_config = ConfigDict(
extra="forbid",
json_schema_extra={
"examples": [
{
"subject": "Charged twice for March",
"body": "My card was charged two times for the same invoice.",
}
]
},
)
subject: str = Field(
min_length=1, max_length=120, description="The short title of the ticket."
)
body: str = Field(
default="", max_length=5000, description="The customer's message. Optional."
)
class Classification(BaseModel):
"""The predicted category and priority of a ticket."""
model_config = ConfigDict(
json_schema_extra={
"examples": [
{
"category": "billing",
"priority": 1,
"confidence": 0.9,
"model_version": "keywords-1.0",
}
]
}
)
category: Category = Field(description="The team that should handle the ticket.")
priority: int = Field(ge=1, le=3, description="1 is the most urgent, 3 the least.")
confidence: float = Field(
ge=0, le=1, description="How sure the classifier is, from 0 to 1."
)
model_version: str = Field(description="The classifier that made the prediction.")
class CategoryList(BaseModel):
categories: list[Category] = Field(
description="Every category that /v1/classify can return."
)
class Health(BaseModel):
status: Literal["ok"]
class ErrorDetail(BaseModel):
code: str = Field(description="A stable code that a program can check.")
message: str = Field(description="A short explanation for a person.")
fields: list[str] = Field(
default=[], description="The request fields that are not valid, if any."
)
class ErrorResponse(BaseModel):
model_config = ConfigDict(
json_schema_extra={
"examples": [
{
"error": {
"code": "invalid_request",
"message": "The request is not valid.",
"fields": ["subject"],
}
}
]
}
)
error: ErrorDetail
Partie Apparaît dans la documentation sous cette forme
La chaîne de documentation """A support ticket to classify.""" (un billet de soutien à classer) La description du schéma.
description="..." dans Field Une ligne sous chaque champ du schéma.
json_schema_extra={"examples": [...]} Le corps donné en exemple. Swagger UI le place dans la zone de Try it out (essayer). Un développeur peut ainsi envoyer une vraie requête utile en un clic.
Health Un modèle pour /health, afin de documenter sa réponse comme les autres.

Choisissez des exemples qui fonctionnent et qui enseignent : l’exemple de TicketIn renvoie billing (facturation) avec la priorité 1. Il montre ce que fait l’API. Un exemple comme {"subject": "string"} renvoie other (autre), ce qui ne montre rien.

Sur la route, un résumé, une description et la vraie réponse d’erreur :

ticket_api/routes.py (changes)
from ticket_api.models import (
Category,
CategoryList,
Classification,
ErrorResponse,
TicketIn,
)
@router.get("/categories", summary="List the ticket categories")
def list_categories() -> CategoryList:
return CategoryList(categories=list(get_args(Category)))
@router.post(
"/classify",
summary="Classify a support ticket",
description=(
"Predicts the category and priority of one ticket from its subject and body. "
"The same text always gives the same result."
),
response_description="The predicted category and priority",
responses={
422: {
"model": ErrorResponse,
"description": "A field is missing or not valid.",
},
},
)
def classify(
# ... the function does not change

responses={422: {"model": ErrorResponse, ...}} remplace le schéma 422 par défaut de FastAPI par la structure réellement envoyée par l’API. Cela corrige la lacune la plus importante de la dernière leçon.

Dans main.py, l’application reçoit une version, une description et une description pour chaque étiquette. /health reçoit sa propre étiquette et un modèle :

ticket_api/main.py (changes)
from ticket_api.models import Health
DESCRIPTION = """
Classifies support tickets into a category and a priority.
The classifier is a deterministic mock: it uses keywords, so the same text
always gives the same result. Every error has the same shape:
`{"error": {"code": ..., "message": ..., "fields": [...]}}`.
"""
app = FastAPI(
title="Ticket Classifier API",
version="1.0.0",
description=DESCRIPTION,
openapi_tags=[
{"name": "tickets", "description": "Classify support tickets."},
{"name": "health", "description": "Check that the service runs."},
],
)
@app.get("/health", tags=["health"], summary="Check that the service runs")
def health() -> Health:
return Health(status="ok")

La description est en Markdown, donc `...` s’affiche comme du code. C’est la première chose qu’un développeur lit. Elle présente donc les deux faits dont chaque client a besoin : les résultats sont déterministes et toutes les erreurs ont la même structure. version="1.0.0" est la version du contrat de l’API, pas celle du code. Modifiez-la quand le contrat change.

Swagger UI pour Ticket Classifier API 1.0.0. Sous le titre, la description explique le classificateur et la structure des erreurs. La section tickets, décrite par Classify support tickets (classer des billets de soutien), contient GET /v1/categories List the ticket categories (lister les catégories de billets) et POST /v1/classify Classify a support ticket (classer un billet de soutien). La section health, décrite par Check that the service runs (vérifier que le service fonctionne), contient GET /health. Les schémas sont CategoryList, Classification, ErrorDetail, ErrorResponse, Health et TicketIn.
Après : une version, une description, des sections décrites et des résumés. HTTPValidationError ne figure plus dans les schémas. L’image est en anglais.
POST /v1/classify ouvert. La description indique qu'il prédit la catégorie et la priorité d'un billet et que le même texte donne toujours le même résultat. Le corps donné en exemple contient un vrai sujet et un vrai texte. La réponse 200, The predicted category and priority (la catégorie et la priorité prédites), présente l'exemple suivant : category billing, confidence 0.9, model_version keywords-1.0, priority 1. La réponse 422, A field is missing or not valid (un champ est manquant ou invalide), présente un exemple d'erreur avec code invalid_request, fields subject et message The request is not valid (la requête est invalide).
Le point de terminaison explique maintenant son fonctionnement, et chaque exemple est une vraie requête ou réponse. L'exemple 422 correspond à ce que l'API envoie. L’image est en anglais.
  1. Remplacez models.py par l’exemple détaillé.
  2. Modifiez routes.py et main.py. Enregistrez, puis rechargez /docs.
  3. Vérifiez la vue d’ensemble : la version est 1.0.0, la description se trouve sous le titre et /health a sa propre section.
  4. Ouvrez POST /v1/classify, sélectionnez Try it out, puis Execute (exécuter) sans modifier le corps donné en exemple. La réponse est billing, avec la priorité 1.
  5. Remplacez le corps par {"subject": ""}, puis exécutez la requête. Comparez la réponse à l’exemple 422. Ils ont maintenant la même structure.
  6. Ouvrez Schemas › Classification (schémas › classification) et développez-le. Chaque champ a sa description.

À vous de jouer : documenter le point de terminaison des catégories

Section intitulée « À vous de jouer : documenter le point de terminaison des catégories »

Un développeur d’interface doit créer un menu de catégories à partir de GET /v1/categories, avec seulement /docs. Améliorez sa documentation :

  1. Une description sur la route qui indique quand l’appeler, par exemple une fois au chargement de la page.
  2. Un response_description.
  3. Un exemple sur CategoryList.
  4. Une phrase dans la description indiquant que other signifie « aucune catégorie ne correspond ». Le menu peut ainsi l’omettre au besoin.

Rechargez ensuite /docs et vérifiez chaque modification comme le ferait une personne qui découvre l’API.

Afficher une réponse
ticket_api/routes.py
@router.get(
"/categories",
summary="List the ticket categories",
description=(
"Returns every category that /v1/classify can return, in a fixed order. "
"Call it once, for example when a page loads. "
"`other` means that no category matched the ticket."
),
response_description="The categories",
)
ticket_api/models.py
class CategoryList(BaseModel):
model_config = ConfigDict(
json_schema_extra={
"examples": [
{"categories": ["billing", "login", "shipping", "account", "other"]}
]
}
)
categories: list[Category] = Field(
description="Every category that /v1/classify can return."
)

Notez que l’exemple doit rester exact quand les catégories changent. Dans la prochaine leçon, un test vérifie que les exemples documentés fonctionnent encore.

Erreur courante

Un exemple documenté que l'API rejette

Symptôme : un développeur sélectionne Try it out, envoie l’exemple de /docs sans modification et obtient 422. Il conclut que l’API ne fonctionne pas.

Cause : l’exemple ne correspond plus au modèle. Quelqu’un a renommé un champ (par exemple subject en title), réduit une limite ou ajouté un champ obligatoire sans mettre à jour json_schema_extra. Pydantic ne vérifie pas les exemples. Ce sont seulement des textes dans le document.

Correction :

  1. Envoyez vous-même chaque exemple après chaque modification d’un modèle.
  2. Mieux encore : faites-le avec un test, pour qu’un exemple incorrect fasse échouer la compilation. Vous écrirez ce test dans la prochaine leçon.
  3. Gardez les exemples près des champs qu’ils illustrent, dans le modèle. Une modification de l’un sera ainsi proche de l’autre.

Vérifiez votre compréhension

Pourquoi l'exemple d'erreur utilise-t-il un vrai code, invalid_request, plutôt que de laisser Swagger UI en générer un?

Afficher la réponse

Un exemple généré utilise seulement le type. Il affiche donc "code": "string", ce qui ne renseigne pas le développeur sur les valeurs attendues. Un vrai exemple montre le code à vérifier dans son programme et la vraie structure de fields. Les exemples sont souvent la première et la seule partie de la documentation que les développeurs lisent.

Vérifiez votre compréhension

Vous modifiez le modèle de réponse et ajoutez un champ facultatif explanation. Quel numéro dans FastAPI(...) modifiez-vous, et comment?

Afficher la réponse

version. Ajouter un champ de réponse facultatif ne rompt pas la compatibilité. C’est donc une petite évolution, par exemple de 1.0.0 à 1.1.0. Une modification incompatible aurait une nouvelle version dans le chemin (/v2) et une nouvelle version majeure, 2.0.0. La version indique aux clients quel contrat ils consultent.

Récapitulatif

  • Chaque partie de /docs vient de votre code : l’application, les routes et les modèles.
  • Décrivez ce que les valeurs signifient, pas seulement leurs types.
  • Donnez à chaque modèle un vrai exemple qui fonctionne et qui enseigne. Swagger UI l’utilise pour Try it out.
  • Documentez chaque réponse d’erreur avec son vrai modèle : responses={422: {"model": ErrorResponse, ...}}.
  • La version est la version du contrat.

Ensuite, vous ferez échouer un test quand la documentation et l’API ne correspondront plus.

Leçon suivante : Garder la documentation exacte