Les spécifications OpenAPI vous disent à quoi ressemble une requête valide et à quoi ressemble une réponse valide. Elles ne vous disent pas si vous êtes autorisé à appeler POST /orders avant POST /auth, ou ce qui se passe si vous appelez GET /invoice/{id} après DELETE /invoice/{id}. Cette information réside dans une spécification de protocole, et OpenAPI n’est pas une spécification de protocole.
C’est le manque. Vous pouvez générer chaque type de message dans votre système à partir d’un document OpenAPI. Vous ne pouvez pas générer les règles sur quand ces messages sont autorisés à circuler. Ces règles sont le session type, et elles résident dans une couche d’abstraction différente.
Ce qu’OpenAPI capture et ce qu’il ignore
OpenAPI est un contract pour la surface d’exposition HTTP. Il définit les chemins, les méthodes, les paramètres de requête, les corps de requête, les codes de réponse, et les schémas JSON. C’est précieux. C’est aussi fondamentalement statique. Chaque endpoint est décrit de manière isolée. Les relations entre les endpoints, les transitions d’état qu’ils déclenchent, et les séquences qui sont légales sont laissées comme exercice pour le lecteur.
Les session types sont l’opposé. Un session type est une description formelle d’un protocole de communication. Il spécifie l’ordre dans lequel les messages doivent être envoyés et reçus, qui envoie quoi, et comment le protocole se branche en fonction du contenu du message. Un session type binaire pourrait dire : le client envoie un message Login, puis le serveur répond soit avec Success et le protocole continue, soit avec Failure et le protocole se termine.
Vous ne pouvez pas dériver cette séquence d’une spécification OpenAPI car la séquence n’a jamais été écrite. Le document OpenAPI liste les endpoints. Il ne spécifie pas que /auth doit précéder /orders. Cette contrainte réside dans la documentation, le code, ou dans la tête de l’ingénieur qui a conçu l’API.
Ce que vous pouvez extraire : la couche message
Ce qu’OpenAPI vous donne vraiment, ce sont des définitions de messages précises et lisibles par machine. Chaque schéma de corps de requête, chaque schéma de réponse, chaque enum et discriminator est spécifié en JSON Schema. C’est la matière première pour les types de messages dans une spécification de session type.
Voici un exemple concret. Considérez ce fragment OpenAPI :
paths:
/auth:
post:
requestBody:
content:
application/json:
schema:
type: object
properties:
username: { type: string }
password: { type: string }
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
token: { type: string }
'401':
description: Unauthorized
À partir de cela, vous pouvez extraire deux types de messages : un AuthRequest contenant un nom d’utilisateur et un mot de passe, et un AuthResponse qui est soit un Success avec un token, soit un Failure. Le session type a besoin de ces types. Il a aussi besoin de savoir que le client envoie la requête et que le serveur envoie la réponse, ce qu’OpenAPI implique par la sémantique HTTP.
Vous pouvez automatiser cette extraction. Voici un script Python qui analyse une spécification OpenAPI JSON et génère des définitions de messages de style Rust :
import json
from dataclasses import dataclass
@dataclass
class Field:
name: str
type: str
@dataclass
class Message:
name: str
fields: list[Field]
TYPE_MAP = {"string": "String", "integer": "i64", "boolean": "bool"}
def sanitize_name(path: str, method: str, suffix: str) -> str:
cleaned = path.replace("/", "").replace("{", "").replace("}", "")
return f"{method.upper()}{cleaned}{suffix}"
def extract_messages(openapi_path: str) -> list[Message]:
with open(openapi_path) as f:
spec = json.load(f)
messages = []
for path, methods in spec.get("paths", {}).items():
for method, operation in methods.items():
if not isinstance(operation, dict):
continue
req = operation.get("requestBody", {})
schema = req.get("content", {}).get("application/json", {}).get("schema", {})
if schema:
fields = [
Field(name=k, type=TYPE_MAP.get(v.get("type"), "serde_json::Value"))
for k, v in schema.get("properties", {}).items()
]
messages.append(Message(name=sanitize_name(path, method, "Request"), fields=fields))
for code, resp in operation.get("responses", {}).items():
schema = resp.get("content", {}).get("application/json", {}).get("schema", {})
if schema:
fields = [
Field(name=k, type=TYPE_MAP.get(v.get("type"), "serde_json::Value"))
for k, v in schema.get("properties", {}).items()
]
messages.append(Message(name=sanitize_name(path, method, f"Response{code}"), fields=fields))
return messages
if __name__ == "__main__":
for m in extract_messages("api.json"):
print(f"struct {m.name} {{")
for f in m.fields:
print(f" {f.name}: {f.type},")
print("}")
C’est mécanique, mais ça fonctionne. Il transforme votre document OpenAPI en structs qu’une implémentation de session type peut référencer. Les types sont exacts. Les noms sont littéraux. Les relations manquent.
Où réside l’information de séquence
Pour obtenir le protocole, vous devez connaître la state machine. Certaines APIs encodent cela de manière implicite. Un POST /orders retourne un ID de commande, et les appels GET /orders/{id} ultérieurs y font référence. L’état du protocole inclut « une commande a été créée ». Une spécification OpenAPI ne modélise pas cette dépendance.
Il existe des standards émergents qui tentent de combler ce manque. AsyncAPI gère les protocoles orientés événements avec une sémantique de canal, mais il ne vous donne toujours pas le session type global. Smithy définit des operations et des traits, et peut modéliser des finite state machines via des traits personnalisés, mais cela nécessite une annotation explicite. JSON Hyper-Schema a tenté de lier des ressources mais n’a jamais connu une adoption large.
Pour l’instant, l’approche pratique consiste à traiter le protocole comme un artefact séparé. Vous générez les types de messages à partir d’OpenAPI, puis vous écrivez le session type à la main par-dessus.
Un hybride pratique : types générés plus un protocole écrit à la main
Voici ce que cela donne en pratique en utilisant Rust et la crate session_types. D’abord, générez les types de messages à partir d’OpenAPI en utilisant le script ci-dessus ou un outil comme typify. Puis définissez le session type explicitement :
use session_types::*;
// Generated from OpenAPI
struct AuthRequest { username: String, password: String }
struct AuthSuccess { token: String }
struct AuthFailure { reason: String }
struct OrderRequest { item_id: u64, quantity: u64 }
struct OrderConfirmation { order_id: u64 }
// The protocol: authenticate, then optionally place an order
type ServerProto = Receive<AuthRequest, Offer<
Send<AuthSuccess, Receive<OrderRequest, Send<OrderConfirmation, Eps>>>,
Send<AuthFailure, Eps>
>>;
fn server(c: Chan<(), ServerProto>) {
let (c, req) = c.recv();
if authenticate(&req) {
let c = c.sel1().send(AuthSuccess { token: "abc".into() });
let (c, order) = c.recv();
let c = c.send(OrderConfirmation { order_id: 42 });
c.close();
} else {
let c = c.sel2().send(AuthFailure { reason: "bad creds".into() });
c.close();
}
}
Le session type ServerProto spécifie exactement ce que le serveur fait. Il reçoit un AuthRequest. Puis il offre un choix : soit envoyer AuthSuccess et continuer à recevoir un OrderRequest, soit envoyer AuthFailure et se terminer. La spécification OpenAPI nous a donné les structs. Le session type nous a donné la grammaire.
C’est la division du travail. OpenAPI gère les formes des messages. Les session types gèrent l’ordre des messages. L’un est généré, l’autre est conçu.
Les compromis que vous devriez connaître
L’extraction automatisée à partir d’OpenAPI comporte des pièges. La première est le polymorphisme. OpenAPI utilise oneOf et anyOf pour les unions, mais l’information du discriminator ne correspond pas proprement au branchement du session type. Un oneOf dans un schéma de réponse peut représenter deux formes de succès différentes, ou il peut représenter une branche de protocole. Vous devez lire la spécification pour savoir laquelle.
La deuxième est la sémantique propre à HTTP. Les session types sont indépendants du protocole de transport. OpenAPI est profondément lié aux méthodes HTTP, aux codes de statut, et aux en-têtes. Quand vous extrayez les messages, vous perdez l’information de méthode et de chemin à moins de l’encoder dans le nom du message. C’est pourquoi le script Python ci-dessus génère des noms comme POSTAuthResponse200. C’est laid, mais cela préserve la provenance.
La troisième est les spécifications partielles. De nombreux documents OpenAPI sont générés à partir de code et n’incluent pas toutes les réponses d’erreur. Si votre session type doit gérer chaque branche possible, une spécification OpenAPI incomplète produira un session type incomplet. Le type checker ne détectera pas les branches manquantes si les branches n’ont jamais été dans le document source.
Comment implémenter cela dans votre codebase
Commencez par les messages. Utilisez openapi-typescript, typify, ou un script personnalisé pour générer des types spécifiques au langage à partir de vos schémas OpenAPI. N’essayez pas de générer le protocole à ce stade. Contentez-vous d’obtenir les structs.
Ensuite, écrivez le session type pour le flux le plus critique de votre système. Choisissez le flux où un bug de séquencement serait le plus coûteux. L’authentification et le paiement sont de bons candidats. Définissez le session type dans le langage de votre choix. Rust a session_types. Il existe des bibliothèques expérimentales pour TypeScript, OCaml, et Scala. Si votre langage n’a pas de bibliothèque de session types, écrivez un test de state machine qui affirme les séquences valides. Ce n’est pas la même garantie, mais cela détecte la même classe de bugs.
Enfin, gardez les deux artefacts synchronisés avec la CI. Quand la spécification OpenAPI change, régénérez les types de messages. Si un nouveau champ obligatoire est ajouté, le code du session type échouera à la compilation. C’est la sécurité que vous achetez.
FAQ
Cela fonctionne-t-il pour les APIs WebSocket ou gRPC ?
Pas directement. OpenAPI décrit HTTP. Pour gRPC, vous avez des schémas protobuf, qui vous donnent les types de messages mais toujours pas les séquences. Pour les WebSockets, AsyncAPI est un meilleur ajustement, mais la même limitation s’applique : il décrit des canaux, pas des protocoles de session globaux.
Puis-je générer des session types à partir d’AsyncAPI à la place ?
AsyncAPI ajoute une sémantique de canal et un routage de messages, ce qui vous rapproche. Il ne spécifie toujours pas la state machine globale du protocole. Vous extrairiez les types de messages d’AsyncAPI et écririez le session type à la main, comme avec OpenAPI.
Et si mon API a cinquante endpoints ?
Cinquante endpoints signifient cinquante types de messages, pas un session type avec cinquante étapes. Les vrais protocoles se décomposent en sessions plus petites. Une API de e-commerce pourrait avoir des sessions séparées pour l’authentification, le paiement, et l’inventaire. Composez-les.
Existe-t-il un outil qui fait tout cela automatiquement ?
Pas encore. La recherche existe. Il existe des papiers sur l’extraction de session types à partir d’APIs REST et de choreographies. Des outils prêts pour la production, il n’en existe pas. Pour l’instant, l’approche hybride est la voie pragmatique.
Votre spécification OpenAPI est un dictionnaire. Elle vous dit ce que les mots signifient. Un session type est une grammaire. Elle vous dit quelles phrases sont légales. Générez le vocabulaire à partir d’OpenAPI. Écrivez la grammaire vous-même.