openapi-trait
A Rust proc-macro attribute that reads an OpenAPI specification file at compile time and generates typed Rust traits from it, so you can implement your API server or define a transport-agnostic client contract with full type safety and no boilerplate.
use PetstoreApi as _;
;
;
// Wire up an axum router.
let app = MyServer.router.with_state;
;
What gets generated
For every OpenAPI spec the macro emits inside the target module:
| Generated item | Source |
|---|---|
Structs with serde derives |
components/schemas |
{OperationId}Request structs |
Path, query, header params + request body per operation |
Per-operation {OperationId}Response enums |
HTTP status codes per operation |
impl axum::response::IntoResponse |
For every response enum generated by openapi_trait::axum |
{ModName}Api<S = ()> trait |
One method per operationId for server implementations |
{ModName}Client trait |
One method per operationId for transport-agnostic client implementations |
router method on the trait |
Wires all operations to an axum::Router when using openapi_trait::axum |
NotImplemented marker struct |
Emitted by openapi_trait::axum; trait default method bodies return Err(NotImplemented.into()), so Self::Error must satisfy From<NotImplemented> |
Crates
| Crate | Purpose |
|---|---|
openapi-trait |
Main entry point — add this to your Cargo.toml |
openapi-trait-axum |
Axum proc-macro — not for direct use |
openapi-trait-client |
Client proc-macro and ReqwestClient derive — not for direct use |
openapi-trait-shared |
Framework-agnostic codegen helpers — not for direct use |
Usage
Add to Cargo.toml:
[]
= "0.1"
Then apply the macro to a mod block:
Or generate a transport-agnostic client trait:
The path is resolved relative to the crate root (CARGO_MANIFEST_DIR). The
file is tracked by Cargo — the crate recompiles automatically when the spec
changes.
The generated trait name comes from the module name, so mod petstore {}
produces petstore::PetstoreApi and petstore::PetstoreClient.
Reqwest client support
openapi-trait enables the reqwest-client feature by default. That adds:
#[derive(openapi_trait::ReqwestClient)]for carrier structs that hold areqwest::Client- A blanket implementation of the generated
{ModName}Clienttrait for any type implementingopenapi_trait::ReqwestClientCore - Re-exports of
reqwestandpercent_encodingfor generated client code
Per-request headers and authentication
Every generated client method takes an Option<openapi_trait::RequestOptions>
argument in addition to the operation request. Use it to attach extra headers or
authentication to a single call without re-instantiating the client. Pass None
when you have nothing to add:
use PetstoreClient as _;
// No extras:
client
.get_pet_by_id
.await?;
// Bearer auth + a custom header, scoped to this request only:
client
.get_pet_by_id
.await?;
RequestOptions also offers basic_auth(username, password). Options are
applied after the operation's declared headers.
Disable default features if you only want the transport-agnostic trait:
[]
= { = "0.1", = false }
OpenAPI support
| Feature | Status |
|---|---|
components/schemas → structs |
✅ |
String format → specialized types |
✅ — date-time → chrono::DateTime<Utc>, date → chrono::NaiveDate, uuid → uuid::Uuid, binary → Vec<u8>; email/others → String (chrono/uuid re-exported from the facade) |
| Path parameters | ✅ |
| Query parameters (including string enums) | ✅ |
| Header parameters | ✅ |
| Request bodies (JSON) | ✅ |
| Response enums per operation | ✅ |
oneOf |
✅ — tagged enum when a discriminator is present, otherwise #[serde(untagged)] |
allOf |
✅ — merged struct ($ref branches via #[serde(flatten)], inline objects inlined) |
anyOf |
✅ — #[serde(untagged)] enum |
| Inline compositions in object properties | ✅ — hoisted to a top-level type named {ParentStruct}{Property} |
not / unconstrained any |
Falls back to serde_json::Value |
| Security schemes | ✅ — apiKey (header/query/cookie) and http (bearer + basic); oauth2 / openIdConnect recognised but skipped |
Security
For each scheme declared in components.securitySchemes, a typed struct is generated (named after the scheme key, PascalCased): apiKey-style schemes become a newtype pub struct Foo(pub String), http basic becomes pub struct Foo { username, password }. Operation-level security overrides the document-level default; security: [] disables auth on an operation; an OR of alternatives generates an {Op}Auth enum with one variant per scheme.
Server (axum) — handlers receive credentials as an extra auth parameter; the framework only extracts the raw value, the handler validates:
async
Missing credentials return 401 Unauthorized before the handler runs.
Client (reqwest) — credentials live on the client carrier and are injected on every call. Add a field for the generated {Mod}AuthState and mark it #[openapi_trait(auth)]; the generated {Mod}ClientAuth extension trait supplies fluent with_<scheme>(...) setters:
use ApiClientAuth as _;
let client = MyClient .with_bearer_auth;
Server and client signatures differ intentionally: server handlers see credentials per-call (so RBAC decisions can vary by request); clients carry them session-wide.
Debugging generated code
To inspect what the macro produces, set the OPENAPI_TRAIT_DEBUG environment variable at build time. Each annotated module is written to a prettyprinted <module_name>.rs file:
OPENAPI_TRAIT_DEBUG=1
The value controls where the files go:
OPENAPI_TRAIT_DEBUG |
Behaviour |
|---|---|
unset, empty, 0, false |
Disabled (default) |
1, true |
Write to $OUT_DIR/openapi-trait-debug, or <temp dir>/openapi-trait-debug if there is no build script |
| any other value | Treated as the target directory path, e.g. OPENAPI_TRAIT_DEBUG=./gen |
The resolved path of each file is printed to stderr during the build. Write failures are reported but never abort compilation.
Unlike cargo expand, this dumps only the code the macro emits directly — nested derives (Serialize, Deserialize, …) are left as #[derive(...)] rather than recursively expanded — which keeps the output focused on this crate's code generation. The macro re-runs only when the crate is recompiled, so use cargo clean -p <crate> or edit the spec to force a fresh dump.
Note that the filename is just the module name, so two mod petstore { … } declarations writing to the same directory will overwrite each other; point OPENAPI_TRAIT_DEBUG at a per-build directory to keep them separate.