Спецификации OpenAPI говорят, как выглядит корректный запрос и как выглядит корректный ответ. Они не говорят, можно ли вызывать POST /orders до POST /auth, или что произойдёт, если вызвать GET /invoice/{id} после DELETE /invoice/{id}. Эта информация содержится в спецификации протокола, а OpenAPI — это не спецификация протокола.
Вот в чём пробел. Из документа OpenAPI можно сгенерировать все типы сообщений в вашей системе. Но нельзя сгенерировать правила о том, когда эти сообщения разрешено отправлять. Эти правила — session type, и они находятся на другом уровне абстракции.
Что OpenAPI фиксирует, а что игнорирует
OpenAPI — это contract для HTTP-интерфейса. Он определяет пути, методы, query-параметры, тела запросов, коды ответов и JSON-схемы. Это ценно. Но это фундаментально статично. Каждый эндпоинт описан изолированно. Связи между эндпоинтами, переходы состояний, которые они инициируют, и допустимые последовательности оставлены в качестве упражнения для читателя.
Session types — противоположность. Session type — это формальное описание коммуникационного протокола. Он определяет порядок, в котором сообщения должны отправляться и приниматься, кто что отправляет, и как протокол ветвится в зависимости от содержимого сообщения. Бинарный session type может говорить: клиент отправляет сообщение Login, затем сервер либо отвечает Success, и протокол продолжается, либо отвечает Failure, и протокол завершается.
Нельзя вывести эту последовательность из спецификации OpenAPI, потому что последовательность никогда не была записана. Документ OpenAPI перечисляет эндпоинты. Он не указывает, что /auth должен предшествовать /orders. Это ограничение живёт в документации, коде или в голове инженера, который спроектировал API.
Что можно извлечь: слой сообщений
Что OpenAPI действительно даёт — это точные, машиночитаемые определения сообщений. Каждая схема тела запроса, каждая схема ответа, каждый enum и дискриминатор заданы в JSON Schema. Это сырьё для типов сообщений в спецификации session type.
Вот конкретный пример. Рассмотрим этот фрагмент 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
Из этого можно извлечь два типа сообщений: AuthRequest, содержащий имя пользователя и пароль, и AuthResponse, который либо Success с токеном, либо Failure. Session type нуждается в этих типах. Также нужно знать, что клиент отправляет запрос, а сервер — ответ, что OpenAPI подразумевает через семантику HTTP.
Это извлечение можно автоматизировать. Вот скрипт на Python, который парсит JSON-спецификацию OpenAPI и генерирует определения сообщений в стиле 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("}")
Это механическая работа, но она работает. Он превращает ваш документ OpenAPI в структуры, на которые может ссылаться реализация session type. Типы точны. Имена буквальны. Связи отсутствуют.
Где живёт информация о последовательности
Чтобы получить протокол, нужно знать конечный автомат. Некоторые API кодируют это неявно. POST /orders возвращает идентификатор заказа, а последующие вызовы GET /orders/{id} ссылаются на него. Состояние протокола включает «заказ создан». Спецификация OpenAPI не моделирует эту зависимость.
Появляются стандарты, которые пытаются преодолеть этот разрыв. AsyncAPI обрабатывает event-driven протоколы с семантикой каналов, но всё равно не даёт глобальный session type. Smithy определяет операции и traits, и может моделировать конечные автоматы через custom traits, но требует явной аннотации. JSON Hyper-Schema пытался связывать ресурсы, но так и не получил широкого распространения.
Пока что практичный подход — рассматривать протокол как отдельный артефакт. Вы генерируете типы сообщений из OpenAPI, а затем вручную пишете session type поверх них.
Практический гибрид: сгенерированные типы плюс рукописный протокол
Вот как это выглядит на практике с использованием Rust и крейта session_types. Сначала сгенерируйте типы сообщений из OpenAPI с помощью скрипта выше или инструмента вроде typify. Затем явно определите session type:
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();
}
}
Session type ServerProto точно определяет, что делает сервер. Он принимает AuthRequest. Затем он предлагает выбор: либо отправить AuthSuccess и продолжить приём OrderRequest, либо отправить AuthFailure и завершиться. Спецификация OpenAPI дала нам структуры. Session type дал нам грамматику.
Это разделение труда. OpenAPI отвечает за формы сообщений. Session types отвечают за порядок сообщений. Одно генерируется, другое проектируется.
Компромиссы, о которых стоит знать
Автоматическое извлечение из OpenAPI имеет острые грани. Первое — полиморфизм. OpenAPI использует oneOf и anyOf для объединений, но информация о дискриминаторе не чисто отображается на ветвление session type. oneOf в схеме ответа может представлять два разных варианта успеха, или может представлять ветвление протокола. Нужно читать спецификацию, чтобы понять, что именно.
Второе — специфичная для HTTP семантика. Session types независимы от транспорта. OpenAPI глубоко привязана к HTTP-методам, статус-кодам и заголовкам. При извлечении сообщений вы теряете информацию о методе и пути, если не закодируете её в имени сообщения. Поэтому скрипт на Python выше генерирует имена вроде POSTAuthResponse200. Это некрасиво, но сохраняет происхождение.
Третье — неполные спецификации. Многие документы OpenAPI генерируются из кода и не включают все ошибочные ответы. Если ваш session type должен обрабатывать каждую возможную ветвь, неполная спецификация OpenAPI даст неполный session type. Type checker не поймает отсутствующие ветви, если их не было в исходном документе.
Как внедрить это в вашем codebase
Начните с сообщений. Используйте openapi-typescript, typify или собственный скрипт для генерации типов под конкретный язык из ваших схем OpenAPI. Не пытайтесь генерировать протокол на этом этапе. Просто получите структуры.
Затем напишите session type для самого критичного потока в вашей системе. Выберите поток, где ошибка в последовательности обошлась бы дороже всего. Аутентификация и оплата — хорошие кандидаты. Определите session type на языке по вашему выбору. В Rust есть session_types. Есть экспериментальные библиотеки для TypeScript, OCaml и Scala. Если в вашем языке нет библиотеки session types, напишите тест конечного автомата, который проверяет допустимые последовательности. Это не тот же уровень гарантий, но ловит тот же класс багов.
Наконец, поддерживайте два артефакта в синхронизации с помощью CI. Когда спецификация OpenAPI меняется, перегенерируйте типы сообщений. Если добавлено новое обязательное поле, код session type не скомпилируется. Это та безопасность, которую вы приобретаете.
FAQ
Работает ли это для WebSocket или gRPC API?
Не напрямую. OpenAPI описывает HTTP. Для gRPC у вас есть protobuf-схемы, которые дают типы сообщений, но всё ещё не последовательности. Для WebSockets AsyncAPI подходит ближе, но то же ограничение применимо: она описывает каналы, а не глобальные session-протоколы.
Можно ли вместо этого генерировать session types из AsyncAPI?
AsyncAPI добавляет семантику каналов и маршрутизацию сообщений, что приближает вас к цели. Но всё равно не определяет глобальный протокольный конечный автомат. Вы бы извлекли типы сообщений из AsyncAPI и написали session type вручную, как и с OpenAPI.
А что если у моего API пятьдесят эндпоинтов?
Пятьдесят эндпоинтов означают пятьдесят типов сообщений, а не один session type с пятьюдесятью шагами. Реальные протоколы раскладываются на меньшие сессии. E-commerce API может иметь отдельные сессии для аутентификации, оформления заказа и инвентаризации. Компонуйте их.
Есть ли инструмент, который делает всё это автоматически?
Пока нет. Исследования существуют. Есть работы по извлечению session types из REST API и из хореографий. Production-ready инструментов нет. Пока что гибридный подход — прагматичный путь.
Ваша спецификация OpenAPI — это словарь. Она говорит, что значат слова. Session type — это грамматика. Она говорит, какие предложения допустимы. Генерируйте словарный запас из OpenAPI. Грамматику пишите сами.