Spec

The reserved benzene:spec topic lets a Benzene service serve its own contract document — and, as .NET extensions, OpenAPI and AsyncAPI. Add it to a message pipeline with the UseSpec middleware extension (Benzene.Schema.OpenApi):

app.UseBenzeneMessage(benzeneMessageApp => benzeneMessageApp
    .UseSpec()
    .UseMessageHandlers());

Over HTTP the default route is GET /benzene/spec (UseBenzeneCloudService maps it; otherwise map it yourself), with type/format as query parameters.

Making a Spec Request

{
  "topic": "benzene:spec",
  "body": "{\"type\":\"asyncapi\",\"format\":\"json\"}"
}
Field Options
type benzene (the contract document, the default), openapi, asyncapi
format json (the default), yaml

An absent body, an empty body, null and {} all ask for benzene/json. Values are compared case-insensitively (YAML is yaml). A type or format the service does not support is answered not-implemented — never with a different document — so a tool can tell "not offered" from "here it is". SpecBuilder.IsSupported(request) answers the same question in code.

The contract document

The benzene format is the language-neutral Contract Document (contract-document.md), derived from the service's registrations:

{
  "schemaDialect": "openapi-3.0",
  "service": "orders",
  "serviceVersion": "1.4.2",
  "versionScheme": "semver",
  "description": "Order intake",
  "messageEndpoint": "/benzene/invoke",
  "transports": ["http", "sqs"],
  "contractHash": "sha256:…",
  "registrationHash": "sha256:…",
  "requests": [
    { "topic": "order:create",
      "request": { "$ref": "#/components/schemas/CreateOrder" },
      "response": { "$ref": "#/components/schemas/OrderDto" } }
  ],
  "events": [
    { "topic": "order:placed", "message": { "$ref": "#/components/schemas/OrderPlaced" } }
  ],
  "sends": [
    { "topic": "payments:capture", "request": { "$ref": "#/components/schemas/CapturePayment" } }
  ],
  "components": { "schemas": { } }
}

Schemas

Payload schemas are OpenAPI 3.0 Schema Objects derived from the CLR types by Swashbuckle, with the dialect rules of contract-document.md §4.2: a property the serializer always writes is required (Benzene's System.Text.Json serializer writes nulls, so every property is, unless it carries [JsonIgnore(Condition = WhenWritingNull)]); a nullable reference to a named type is {"type":"object","nullable":true,"allOf":[{"$ref":…}]}; records carry no additionalProperties; enums keep their values; format (int64, uuid, …) is written where known.

Component names

A named type's component name is its simple name (Page<Order> → OrderPage). Two different types that would share a name fail derivation, naming both types, the topics that use them, and the fix - declare a distinct name with [SchemaName("…")] (Benzene.Abstractions.Schema):

[SchemaName("PaymentItem")]
public record Item(string PaymentId);

Derivation happens on the first benzene:spec request. To fail at start-up instead - before any message is handled - opt in with services.AddContractDocumentStartUpCheck(); benzene-descriptor --emit spec fails the build the same way.

UseBenzeneCloudService turns the start-up check on by default. Deriving every schema costs a cold start, so a short-lived host such as an AWS Lambda function can opt out - and rely on the build-time check instead:

app.UseBenzeneCloudService("orders", cloud => cloud
    .WithoutContractDocumentStartUpCheck());

Message endpoint advertisement

When the service exposes a BenzeneMessage-over-HTTP endpoint (UseBenzeneMessage — see Payload Testing), the benzene format advertises it as an optional top-level messageEndpoint field:

{ "schemaDialect": "openapi-3.0", "service": "orders", "messageEndpoint": "/benzene/invoke", "requests": [ ], "…": "…" }

Consumers feature-detect send capability on this field — no field means the service accepts messages only through its normal transports.

Transport advertisement

The benzene format also advertises every transport the service is wired to receive messages over as an optional top-level transports field — sourced from every registered Benzene.Abstractions.MessageHandlers.Info.ITransportInfo at spec-build time (e.g. "sqs", "kafka", "http"), written only when at least one is registered:

{ "schemaDialect": "openapi-3.0", "service": "orders", "transports": ["http", "sqs"], "requests": [ ], "…": "…" }

This is document-level, not per-topic: any wired non-HTTP transport can reach any registered topic (Benzene's topic routing has no per-topic transport filtering), so a per-topic list would just repeat this same array on every request/event. HTTP is the one exception — a topic's actual HTTP reachability is still its own per-topic httpMappings, which requires an explicit [HttpEndpoint] attribute per handler and is unaffected by this field. Both Spec UI and Mesh UI render this as a chip row.