benzene.openapi
An OpenAPI 3.1 document derived from a Benzene handler registry — a sibling projection to the JSON
Schema and Cloud Service Profile the port already derives from that same registry. Distribution:
benzene-openapi (depends on benzene-core and benzene-http).
pip install benzene-openapi
Overview
Benzene is message-topic-based, not REST, so there is nothing to hand a Swagger UI or a client
generator out of the box. openapi_document closes that gap: it projects the same registry the
/benzene/spec document and the mesh ServiceDescriptor read into a standard OpenAPI document,
reusing benzene.core.json_schema for every payload schema — never re-deriving one.
Like ServiceSpec it is a pure, deterministic registry projection with no third-party runtime
dependencies: one POST operation per registered (topic, version) under the profile's
/benzene/invoke base, with every request and response schema reused verbatim from json_schema and
every failure status mapped to its HTTP code through the port's own benzene.http.to_http table.
Mirrors .NET's Benzene.Schema.OpenApi.
Basic usage
from dataclasses import dataclass
from benzene.core import Registry
from benzene.openapi import openapi_document
@dataclass
class PlaceOrder:
sku: str
quantity: int
@dataclass
class OrderPlaced:
order_id: str
registry = Registry().register(
"orders:place", handler, request_type=PlaceOrder, response_type=OrderPlaced
)
document = openapi_document(registry, title="Orders", version="2.1.0")
document is a plain dict[str, Any] — serialize it with json.dumps, serve it, or feed it to any
OpenAPI tool.
openapi_document
openapi_document(
registry: Registry,
*,
title: str = "Benzene service",
version: str = "1.0.0",
server_paths: StandardPaths | None = None,
) -> dict[str, Any]
registry— the handler registry to project. Each definition's declaredrequest_type/response_typesupply the schemas.title/version— the OpenAPIinfoblock.server_paths— abenzene.http.StandardPaths(defaultStandardPaths(), i.e. the/benzeneprefix). Itsinvoke_pathis the base every operation hangs under; passStandardPaths(prefix="/api")to relocate it to/api/invoke.
OPENAPI_VERSION is the emitted spec version, "3.1.0" — chosen over 3.0.x because 3.1 adopts the JSON
Schema 2020-12 dialect that json_schema already derives, so the embedded schemas drop in unchanged
rather than being down-converted.
The mapping
Benzene has no natural resource hierarchy to project, so the faithful anchor is the profile's
well-known invoke endpoint (POST /benzene/invoke), through which every transport invokes a topic
uniformly. Because an OpenAPI path holds at most one POST operation, each (topic, version) becomes
its own sub-path of that base:
POST {prefix}/invoke/{topic}for an unversioned handler, andPOST {prefix}/invoke/{topic}/{version}for a versioned one (mirroring the{version}route segment the HTTP binding already understands).
So the document reads as one browsable operation per topic, while the operations all live under the
single invoke base the wire binding actually multiplexes them through. The invoke base itself comes
from StandardPaths (benzene.http).
Schemas are reused, never re-derived
Every operation's requestBody and success response $ref a component in components.schemas whose
value is exactly benzene.core.json_schema of the topic's request/response type — the same 2020-12
subset the spec and descriptor embed, with wire-naming (orderId) property names and required
tracking the caller's obligation. Failure responses map the Benzene failure statuses to HTTP codes
through benzene.http.to_http and share one BenzeneError problem-details component
({status, detail}, the wire-contract failure envelope), ordered by HTTP code.
Deterministic output
As with ServiceSpec, the document is built in a stable order — topics sorted by (id, version),
paths and component-schema keys sorted lexicographically, responses ordered by HTTP code — so the same
registry always yields an identical, diff-friendly dict.
Emitted document
{
"openapi": "3.1.0",
"info": { "title": "Orders", "version": "2.1.0" },
"paths": {
"/benzene/invoke/orders:place": {
"post": {
"operationId": "ordersPlace",
"summary": "Invoke topic orders:place",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/OrdersPlaceRequest" }
}
}
},
"responses": {
"200": { "description": "ok", "content": { "application/json": {
"schema": { "$ref": "#/components/schemas/OrdersPlaceResponse" } } } },
"400": { "description": "bad-request", "content": { "application/json": {
"schema": { "$ref": "#/components/schemas/BenzeneError" } } } }
// ... one entry per failure status, ordered by HTTP code
}
}
}
},
"components": {
"schemas": {
"BenzeneError": { "type": "object",
"properties": { "status": { "type": "string" }, "detail": { "type": "string" } },
"required": ["status", "detail"] },
"OrdersPlaceRequest": { "type": "object", "properties": {
"sku": { "type": "string" }, "quantity": { "type": "integer" } },
"required": ["sku", "quantity"] },
"OrdersPlaceResponse": { "type": "object", "properties": {
"orderId": { "type": "string" } }, "required": ["orderId"] }
}
}
}
operation_id
The canonical operationId for a (topic, version) — the camelCased topic, with the version suffixed:
from benzene.openapi import operation_id
operation_id("orders:place") # "ordersPlace"
operation_id("orders:place", "v2") # "ordersPlace_v2"
operation_id(topic: str, version: str = "") -> str
It is deterministic per pair. openapi_document uses it internally, and exposes it so a caller can
correlate a topic to the operation it will emit.
Separator-collision disambiguation
Two topics that differ only by separator — orders:place versus orders-place — camel/PascalCase to
the same base name, which would silently overwrite a component schema and emit a duplicate
operationId (OpenAPI requires uniqueness). The raw-topic path still distinguishes them, so
openapi_document disambiguates the derived names against those already emitted: because definitions
are walked in sorted (id, version) order, the first claimant keeps the clean name (ordersPlace,
OrdersPlaceRequest) and later ones gain a _2 / _3 suffix. The output therefore stays valid and
byte-stable regardless of separator collisions.
Exports
openapi_document, operation_id, OPENAPI_VERSION.
See also
benzene.core— theRegistryandjson_schemathis projects; the derivedServiceSpecit is a sibling of.benzene.http—StandardPaths(the invoke base) andto_http(the failure-status → HTTP code mapping) this reuses.benzene.mesh— the meshServiceDescriptor, the third projection of the same registry.