As especificações OpenAPI dizem como uma requisição válida se parece e como uma resposta válida se parece. Elas não dizem se você pode chamar POST /orders antes de POST /auth, ou o que acontece se você chamar GET /invoice/{id} depois de DELETE /invoice/{id}. Essa informação vive em uma especificação de protocol, e OpenAPI não é uma especificação de protocol.

Essa é a lacuna. Você pode gerar todos os tipos de mensagem do seu sistema a partir de um documento OpenAPI. Você não pode gerar as regras sobre quando essas mensagens podem trafegar. Essas regras são o session type, e elas vivem em uma camada de abstração diferente.

O que OpenAPI captura e o que ignora

OpenAPI é um contract para a superfície HTTP. Ele define paths, methods, query parameters, request bodies, response codes e schemas JSON. Isso é valioso. Também é fundamentalmente estático. Cada endpoint é descrito de forma isolada. Os relacionamentos entre endpoints, as transições de estado que eles disparam e as sequências que são legais são deixados como exercício para o leitor.

Session types são o oposto. Um session type é uma descrição formal de um protocol de comunicação. Ele especifica a ordem na qual as mensagens devem ser enviadas e recebidas, quem envia o quê e como o protocol ramifica com base no conteúdo da mensagem. Um session type binário pode dizer: o cliente envia uma mensagem de Login, então o servidor responde com Success e o protocol continua, ou responde com Failure e o protocol termina.

Você não pode derivar essa sequência de uma especificação OpenAPI porque a sequência nunca foi escrita. O documento OpenAPI lista os endpoints. Ele não especifica que /auth deve preceder /orders. Essa restrição vive na documentação, no código ou na cabeça do engenheiro que projetou a API.

O que você pode extrair: a camada de mensagem

O que OpenAPI realmente lhe dá são definições de mensagem precisas e legíveis por máquina. Cada schema de request body, cada schema de resposta, cada enum e discriminator é especificado em JSON Schema. Esse é o material bruto para os tipos de mensagem em uma especificação de session type.

Aqui está um exemplo concreto. Considere esse fragmento 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

A partir disso, você pode extrair dois tipos de mensagem: um AuthRequest contendo um username e password, e um AuthResponse que é ou um Success com um token ou um Failure. O session type precisa desses tipos. Ele também precisa saber que o cliente envia a requisição e o servidor envia a resposta, o que OpenAPI implica através da semântica HTTP.

Você pode automatizar essa extração. Aqui está um script Python que analisa uma especificação OpenAPI JSON e gera definições de mensagem semelhantes às de 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("}")

Isso é mecânico, mas funciona. Ele transforma seu documento OpenAPI em structs que uma implementação de session type pode referenciar. Os tipos são precisos. Os nomes são literais. Os relacionamentos estão faltando.

Onde a informação de sequência vive

Para obter o protocol, você precisa conhecer a state machine. Algumas APIs codificam isso implicitamente. Um POST /orders retorna um ID de pedido, e chamadas subsequentes de GET /orders/{id} o referenciam. O estado do protocol inclui “um pedido foi criado”. Uma especificação OpenAPI não modela essa dependency.

Existem padrões emergentes que tentam preencher essa lacuna. O AsyncAPI lida com protocols orientados a events com semântica de canais, mas ainda não oferece o session type global. O Smithy define operações e traits, e pode modelar state machines finitas através de traits personalizados, mas exige anotação explícita. O JSON Hyper-Schema tentou vincular recursos, mas nunca teve adoção ampla.

Por enquanto, a abordagem prática é tratar o protocol como um artefato separado. Você gera os tipos de mensagem a partir do OpenAPI, depois escreve o session type manualmente sobre eles.

Um híbrido prático: tipos gerados mais um protocol escrito manualmente

É assim que isso se parece na prática usando Rust e a crate session_types. Primeiro, gere os tipos de mensagem a partir do OpenAPI usando o script acima ou uma ferramenta como typify. Depois defina o session type explicitamente:

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();
    }
}

O session type ServerProto especifica exatamente o que o servidor faz. Ele recebe um AuthRequest. Depois oferece uma escolha: ou enviar AuthSuccess e continuar a receber um OrderRequest, ou enviar AuthFailure e terminar. A especificação OpenAPI nos deu as structs. O session type nos deu a gramática.

Essa é a divisão de trabalho. OpenAPI lida com as formas das mensagens. Session types lidam com a ordem das mensagens. Um é gerado, o outro é projetado.

As compensações que você deve conhecer

A extração automatizada a partir do OpenAPI apresenta desafios. A primeira é o polimorfismo. O OpenAPI usa oneOf e anyOf para uniões, mas a informação do discriminator não mapeia de forma limpa para a ramificação de session type. Um oneOf em um schema de resposta pode representar duas formas de sucesso diferentes, ou pode representar um branch de protocol. Você precisa ler a spec para saber qual.

A segunda é a semântica específica de HTTP. Os session types são agnósticos de transporte. O OpenAPI está profundamente ligado a methods, status codes e headers de HTTP. Quando você extrai mensagens, perde a informação de method e path a menos que a codifique no nome da mensagem. É por isso que o script Python acima gera nomes como POSTAuthResponse200. É feio, mas preserva a proveniência.

A terceira são as specs parciais. Muitos documentos OpenAPI são gerados a partir de código e não incluem todas as respostas de erro. Se o seu session type precisa lidar com cada branch possível, uma especificação OpenAPI incompleta produzirá um session type incompleto. O type checker não capturará branches ausentes se os branches nunca estiveram no documento fonte.

Como implementar isso no seu codebase

Comece com as mensagens. Use openapi-typescript, typify ou um script personalizado para gerar tipos específicos de linguagem a partir dos seus schemas OpenAPI. Não tente gerar o protocol nessa etapa. Apenas obtenha as structs.

Em seguida, escreva o session type para o fluxo mais crítico do seu sistema. Escolha o fluxo onde um bug de sequenciamento seria mais caro. Autenticação e pagamento são bons candidatos. Defina o session type na linguagem de sua escolha. Rust tem session_types. Existem bibliotecas experimentais para TypeScript, OCaml e Scala. Se a sua linguagem não tiver uma biblioteca de session type, escreva um state machine test que afirme sequências válidas. Não é a mesma garantia, mas captura a mesma classe de bugs.

Por fim, mantenha os dois artefatos em sincronia com CI. Quando a especificação OpenAPI mudar, regenere os tipos de mensagem. Se um novo campo obrigatório for adicionado, o código do session type falhará na compilação. Essa é a segurança que você está comprando.

FAQ

Isso funciona para APIs WebSocket ou gRPC?

Não diretamente. O OpenAPI descreve HTTP. Para gRPC, você tem schemas protobuf, que lhe dão tipos de mensagem, mas ainda não sequências. Para WebSockets, o AsyncAPI é mais adequado, mas a mesma limitação se aplica: ele descreve canais, não protocols de session globais.

Posso gerar session types a partir do AsyncAPI?

O AsyncAPI adiciona semântica de canais e roteamento de mensagens, o que aproxima mais. Ele ainda não especifica a state machine do protocol global. Você extrairia tipos de mensagem do AsyncAPI e escreveria o session type manualmente, assim como com o OpenAPI.

E se minha API tiver cinquenta endpoints?

Cinquenta endpoints significam cinquenta tipos de mensagem, não um session type com cinquenta passos. protocols reais se decompõem em sessions menores. Uma API de e-commerce pode ter sessions separadas para autenticação, checkout e inventário. Componha-os.

Existe uma ferramenta que faz tudo isso automaticamente?

Ainda não. A pesquisa existe. Há artigos sobre extração de session types a partir de APIs REST e de coreografias. Ferramentas prontas para produção não existem. Por enquanto, a abordagem híbrida é o caminho pragmático.

Sua especificação OpenAPI é um dicionário. Ela diz o que as palavras significam. Um session type é uma gramática. Ele diz quais frases são legais. Gere o vocabulário a partir do OpenAPI. Escreva a gramática você mesmo.