OpenAPI-Spezifikationen sagen dir, wie eine gültige Anfrage aussieht und wie eine gültige Antwort aussieht. Sie sagen dir nicht, ob du POST /orders vor POST /auth aufrufen darfst, oder was passiert, wenn du GET /invoice/{id} nach DELETE /invoice/{id} aufrufst. Diese Information steckt in einer protocol specification, und OpenAPI ist keine protocol specification.
Das ist die Lücke. Du kannst alle Message Types in deinem System aus einem OpenAPI-Dokument generieren. Du kannst die Regeln nicht generieren, wann diese Nachrichten losgeschickt werden dürfen. Diese Regeln sind der Session Type, und sie leben in einer anderen Abstraktionsschicht.
Was OpenAPI erfasst und was es ignoriert
OpenAPI ist ein Contract für die HTTP-Oberfläche. Es definiert Pfade, Methoden, Query-Parameter, Request-Bodies, Response-Codes und JSON-Schemas. Das ist wertvoll. Es ist aber auch im Grundsatz statisch. Jeder Endpunkt wird isoliert beschrieben. Die Beziehungen zwischen Endpunkten, die Zustandsübergänge, die sie auslösen, und die Sequenzen, die zulässig sind, bleiben eine Übung für den Leser.
Session Types sind das Gegenteil. Ein Session Type ist eine formale Beschreibung eines communication protocol. Er legt die Reihenfolge fest, in der Nachrichten gesendet und empfangen werden müssen, wer was sendet und wie das protocol basierend auf dem Nachrichteninhalt verzweigt. Ein binärer Session Type könnte sagen: Der Client sendet eine Login-Nachricht, dann antwortet der Server entweder mit Success und das protocol läuft weiter, oder mit Failure und das protocol endet.
Du kannst diese Sequenz nicht aus einer OpenAPI-Spezifikation ableiten, weil die Sequenz nie niedergeschrieben wurde. Das OpenAPI-Dokument listet die Endpunkte auf. Es legt nicht fest, dass /auth /orders vorausgehen muss. Diese Einschränkung steckt in der Dokumentation, im Code oder im Kopf des Ingenieurs, der die API entworfen hat.
Was du extrahieren kannst: die Nachrichtenschicht
Was OpenAPI dir gibt, sind präzise, maschinenlesbare Nachrichtendefinitionen. Jedes Request-Body-Schema, jedes Response-Schema, jedes Enum und jeder Discriminator ist in JSON Schema spezifiziert. Das ist das Rohmaterial für die Message Types in einer Session-Type-Spezifikation.
Hier ist ein konkretes Beispiel. Betrachte dieses OpenAPI-Fragment:
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
Daraus kannst du zwei Message Types extrahieren: ein AuthRequest mit Benutzername und Passwort und ein AuthResponse, das entweder ein Success mit einem Token oder ein Failure ist. Der Session Type braucht diese Types. Er muss auch wissen, dass der Client die Anfrage sendet und der Server die Antwort sendet, was OpenAPI durch HTTP-Semantik impliziert.
Du kannst diese Extraktion automatisieren. Hier ist ein Python-Skript, das eine OpenAPI-JSON-Spezifikation parst und Rust-ähnliche Message-Definitionen generiert:
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("}")
Das ist mechanisch, aber es funktioniert. Es verwandelt dein OpenAPI-Dokument in Structs, auf die eine Session-Type-Implementierung referenzieren kann. Die Types sind korrekt. Die Namen sind wörtlich. Die Beziehungen fehlen.
Wo die Sequenzinformation lebt
Um das protocol zu bekommen, musst du die State Machine kennen. Einige APIs kodieren das implizit. Ein POST /orders gibt eine Bestell-ID zurück, und nachfolgende Aufrufe von GET /orders/{id} referenzieren sie. Der protocol state beinhaltet „eine Bestellung wurde erstellt“. Eine OpenAPI-Spezifikation modelliert diese dependency nicht.
Es gibt aufkommende Standards, die das überbrücken wollen. AsyncAPI behandelt event-driven protocols mit Channel-Semantik, gibt aber immer noch keinen globalen Session Type vor. Smithy definiert Operationen und Traits und kann Finite State Machines durch Custom Traits modellieren, erfordert aber eine explizite Annotation. JSON Hyper-Schema hat versucht, Ressourcen zu verknüpfen, wurde aber nie breit adoptiert.
Vorerst ist der pragmatische Ansatz, das protocol als separates Artefakt zu behandeln. Du generierst die Message Types aus OpenAPI und schreibst dann den Session Type per Hand darauf.
Ein pragmatischer Hybrid: generierte Types plus handgeschriebenes protocol
So sieht das in der Praxis mit Rust und dem session_types-Crate aus. Zuerst generierst du die Message Types aus OpenAPI mit dem Skript oben oder einem Tool wie typify. Dann definierst du den Session Type explizit:
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();
}
}
Der Session Type ServerProto legt genau fest, was der Server tut. Er empfängt ein AuthRequest. Dann bietet er eine Wahl an: Entweder AuthSuccess senden und mit dem Empfang eines OrderRequest fortfahren, oder AuthFailure senden und enden. Die OpenAPI-Spezifikation hat uns die Structs geliefert. Der Session Type hat uns die Grammatik geliefert.
Das ist die Arbeitsteilung. OpenAPI kümmert sich um Nachrichtenformen. Session Types kümmern sich um Nachrichtenreihenfolge. Eines wird generiert, das andere wird entworfen.
Die Kompromisse, die du kennen solltest
Die automatisierte Extraktion aus OpenAPI hat scharfe Kanten. Die erste ist Polymorphismus. OpenAPI verwendet oneOf und anyOf für Unions, aber die Discriminator-Information lässt sich nicht sauber auf Session-Type-Verzweigungen abbilden. Ein oneOf in einem Response-Schema könnte zwei verschiedene Erfolgsformen darstellen, oder es könnte eine protocol branch darstellen. Du musst die Spezifikation lesen, um zu wissen, welches.
Die zweite ist HTTP-spezifische Semantik. Session Types sind transportunabhängig. OpenAPI ist tief mit HTTP-Methoden, Statuscodes und Headern verknüpft. Wenn du Nachrichten extrahierst, verlierst du die Methoden- und Pfadinformation, es sei denn, du kodierst sie in den Nachrichtennamen. Deshalb generiert das Python-Skript oben Namen wie POSTAuthResponse200. Es ist hässlich, aber es bewahrt die Herkunft.
Die dritte ist unvollständige Spezifikationen. Viele OpenAPI-Dokumente werden aus Code generiert und enthalten nicht alle Fehlerantworten. Wenn dein Session Type jede mögliche Verzweigung behandeln muss, erzeugt eine unvollständige OpenAPI-Spezifikation einen unvollständigen Session Type. Der Type Checker erkennt fehlende Verzweigungen nicht, wenn die Verzweigungen nie im Quelldokument standen.
Wie du das in deinem Codebase umsetzt
Beginne mit den Nachrichten. Verwende openapi-typescript, typify oder ein eigenes Skript, um sprachspezifische Types aus deinen OpenAPI-Schemas zu generieren. Versuche nicht, in diesem Schritt das protocol zu generieren. Besorg dir einfach die Structs.
Als Nächstes schreibst du den Session Type für den kritischsten Ablauf in deinem System. Wähle den Ablauf, bei dem ein Reihenfolgefehler am teuersten wäre. Authentifizierung und Zahlung sind gute Kandidaten. Definiere den Session Type in deiner Sprache der Wahl. Rust hat session_types. Es gibt experimentelle Bibliotheken für TypeScript, OCaml und Scala. Wenn deine Sprache keine Session-Type-Bibliothek hat, schreibe einen State-Machine-Test, der gültige Sequenzen prüft. Es ist nicht dieselbe Garantie, aber er erkennt dieselbe Klasse von Bugs.
Schließlich halte die beiden Artefakte mit CI synchron. Wenn sich die OpenAPI-Spezifikation ändert, regeneriere die Message Types. Wenn ein neues Pflichtfeld hinzugefügt wird, schlägt der Session-Type-Code fehl beim Kompilieren. Das ist die Sicherheit, die du kaufst.
FAQ
Funktioniert das für WebSocket- oder gRPC-APIs?
Nicht direkt. OpenAPI beschreibt HTTP. Für gRPC hast du protobuf-Schemas, die dir Message Types liefern, aber ebenfalls keine Sequenzen. Für WebSockets ist AsyncAPI eine nähere Passform, aber dieselbe Einschränkung gilt: Es beschreibt Channels, nicht globale session protocols.
Kann ich Session Types stattdessen aus AsyncAPI generieren?
AsyncAPI fügt Channel-Semantik und Nachrichtenrouting hinzu, was dich näher heranbringt. Es legt aber immer noch keine globale protocol state machine fest. Du würdest Message Types aus AsyncAPI extrahieren und den Session Type per Hand schreiben, genau wie bei OpenAPI.
Was, wenn meine API fünfzig Endpunkte hat?
Fünfzig Endpunkte bedeuten fünfzig Message Types, nicht ein Session Type mit fünfzig Schritten. Real protocols zerlegen sich in kleinere Sessions. Eine E-Commerce-API könnte separate Sessions für Authentifizierung, Checkout und Inventar haben. Komponiere sie.
Gibt es ein Tool, das das alles automatisch macht?
Noch nicht. Die Forschung existiert. Es gibt Papiere über die Extraktion von Session Types aus REST APIs und aus Choreographien. Produktionsreife Tools existieren nicht. Vorerst ist der hybride Ansatz der pragmatische Weg.
Deine OpenAPI-Spezifikation ist ein Wörterbuch. Sie sagt dir, was Wörter bedeuten. Ein Session Type ist eine Grammatik. Er sagt dir, welche Sätze zulässig sind. Generiere das Vokabular aus OpenAPI. Schreibe die Grammatik selbst.