openapi-to-rust — OpenAPI generator for Rust
openapi-to-rust is an OpenAPI generator for Rust that turns OpenAPI 3.0/3.1 (and experimental 3.2) specifications into strongly-typed structs, async HTTP clients, SSE streaming clients, and opt-in Axum server scaffolding — including for the messy, real-world specs everyone actually ships.
Read the guides and documentation for the quickest path from an OpenAPI document to compiling Rust.
5-second trial
Paste your spec into the browser playground — the real generator compiled to WebAssembly. See the generated Rust instantly and download it as a complete, compilable crate. Nothing is uploaded.
30-second trial
Install the CLI from crates.io, then generate a tiny client from a stable, hosted fixture:
This writes src/generated/{types,client,mod}.rs and the exact dependencies
needed to compile them in src/generated/REQUIRED_DEPS.toml. Use --dry-run
to inspect the plan without writing, and --check in CI to detect stale
committed output.
We originally built this internally at GPU CLI to generate typed Rust clients for OpenAI, Anthropic, Cloudflare, and other large APIs. After battle-testing it against real-world specs with complex union types, discriminated enums, streaming endpoints, and the occasional spec/API drift, we decided to open source it.
The repository contains 55 real-world specs. The supported OpenAPI corpus is 54 specs (one Gitea document is Swagger 2.0 and intentionally skipped). Pull requests compile-check the OpenAI and Anthropic production specs; a scheduled and manually runnable CI tier checks all 54 OpenAPI specs.
Release history and breaking changes live in the changelog.
Highlights
- OpenAPI 3.0 and 3.1, with experimental 3.2 support — handles
type: ["X", "null"],anyOf/oneOf/allOf, discriminated unions,const, inline objects, and accepts paths-less specs (components-only or webhooks-only). - Generates clients and servers — pick client calls with
[client], hosted operations with[server], or keep the default all-operation client. Both share the sametypes.rs. - Typed scalars —
format: date-time→chrono::DateTime<chrono::Utc>,uri→url::Url,binary→bytes::Bytes,uuid→uuid::Uuid,byte→Vec<u8>+ base64 codec, unsigned-int formats →u32/u64. All opt-out per-format in TOML. - Async HTTP client — typed methods per operation, retry/backoff via
reqwest-retry, distributed tracing viareqwest-tracing, Bearer / API-key / custom auth (honored at runtime), default headers, path-template percent-encoding. - Axum server scaffolding — trait per tag, status-code-typed response enum, SSE-ready
OkStreamvariant, required-param HTTP 400 short-circuit at the handler boundary, combinedbuild_router(...)factory for multi-tag selections. - SSE streaming clients — first-class Server-Sent Events with reconnection.
- Smart discriminated unions — auto-detects implicit discriminators from
constproperties, falls back to#[serde(untagged)]when a union mixes scalar and object branches (e.g."auto"or a tagged object). - Per-operation typed errors — each operation gets its own error enum with
Status4xx(...)typed bodies; you can match on the exact API error shape. - Typed
additionalProperties— extra keys becomeBTreeMap<String, T>instead of falling toserde_json::Valuewhen the spec gives a value-type schema. - Constraint-as-doc —
minLength/maxLength/minimum/patternetc. are emitted as/// Constraint: …doc comments. No runtime validation is added, so generated code stays free of validator-crate dependencies. - TOML configuration with overrides for spec quirks (nullable, extensible enums, type aliases).
- Snapshot testing —
instasnapshots for generated output. - Optional
specta::Typederives for cross-language type sharing.
Install
Rust users with Rust 1.88 or newer can install the CLI from crates.io:
This compiles the CLI locally and requires a Rust toolchain. Prebuilt binaries
are not currently published. If you use the generator as a Rust library instead,
run cargo add openapi-to-rust.
Try it in one command
Generate types and an async client directly from a local document or HTTPS URL:
# Writes src/generated/{types,client,mod}.rs and REQUIRED_DEPS.toml.
The async client is the default. Use --types-only to omit it, --dry-run to
analyze without writing, or --check in CI to fail when committed output is
stale. Remote fetches require HTTPS (except loopback development), reject URL
credentials and redirects, and enforce time and response-size limits.
To keep a reusable config:
Quick start — client
openapi-to-rust.toml:
[]
= "openapi.json"
= "src/generated"
= "api"
# All paths above are relative to this TOML file, not the shell's working directory.
[]
# Add `*_builder()` when an operation has more than three optional values.
= true
= 3
[]
= true
[]
= "https://api.example.com"
= 30
[]
= 3
[]
= "Bearer"
= "Authorization"
Then:
Select only the client operations you use
[client] is optional. Without it—or with an empty operations list—the HTTP client keeps every operation for backward compatibility. Selectors share the server grammar: exact operationId, exact METHOD /path, or tag:<name>.
[]
= [
"createResponse",
"GET /v1/models",
"tag:Files",
]
= true
prune_models = true also trims types.rs. When client and server selections coexist, the generator retains the union of schemas reachable from both scopes, including inline generated request, response, and parameter types. [client] is ignored when enable_async_client = false, and it never filters an enabled operation registry.
Quick start — Axum server
Pick the operations you want to host. The generator emits a trait, a typed response enum, and a router factory. You implement the trait; axum does the rest.
[]
= "openai.yaml"
= "src/gen"
= "openai"
[]
= false # server-only
[]
= "axum"
= ["createResponse", "listInputItems"]
# Drop schemas not reachable from the picked operations. Safe when
# you're not also generating the HTTP client (the client would lose types).
= true
Discover and scaffold operations from the CLI:
# List operations in the spec (filter by tag / method / substring)
# Add an operation to [server].operations (preserves TOML formatting)
# Remove
Then implement the trait:
use ;
use CreateResponse;
use Event;
use stream;
;
async
Two complete examples are in the repo:
examples/server-openai-responses—createResponse+listInputItems+usage-costs(multi-tag, body + SSE, four query params, required param 400)examples/server-anthropic-messages—messages_postwith a small overlay that declarestext/event-streamon the 200 (Anthropic's published spec omits it)
Generated Output
| File | Description |
|---|---|
types.rs |
All struct/enum definitions from OpenAPI schemas |
client.rs |
Async HTTP client with typed methods per operation (when enable_async_client) |
streaming.rs |
SSE streaming client with event parsing (when configured) |
server/mod.rs |
Module re-exports for the server (when [server] is set) |
server/api.rs |
trait <Tag>Api { async fn <op>(&self, …) -> <Op>Response; } per tag |
server/errors.rs |
enum <Op>Response { Ok(T), BadRequest(E), …, OkStream(Sse<…>) } with IntoResponse |
server/router.rs |
Per-tag Router factory; combined build_router<…>(…) for multi-tag selections |
mod.rs |
Module declarations + re-exports |
REQUIRED_DEPS.toml |
Complete direct dependencies and crate features for the exact generated modes and selected operations — append or merge into the consuming Cargo.toml |
Generated client usage
use crateHttpClient;
use crate*;
async
What the generated types look like
A tour of patterns the generator emits, from real outputs.
Typed scalars
// format: date-time → chrono::DateTime<chrono::Utc>
pub created_at: DateTime,
pub archived_at: ,
// format: uri → url::Url
pub url: Url,
pub callback_url: ,
// format: binary (multipart) → bytes::Bytes
Binary,
// format: uuid → uuid::Uuid
pub request_id: Uuid,
Typed additionalProperties
pub additional_properties: BTreeMap, // usage maps
pub additional_properties: BTreeMap, // labels
Constraints as doc comments
///Constraint: minLength=1, maxLength=64, pattern=`^[a-zA-Z0-9_-]{1,64}$`
pub custom_id: String,
///Constraint: minimum=0, maximum=1
pub temperature: ,
No runtime validation is generated. The generator never adds the
validatorcrate or#[validate(...)]attributes — constraints are documentation only. Validate at boundaries you control.
Discriminated unions (tagged enums)
Hybrid string-or-object unions
When an anyOf/oneOf mixes a string-enum branch with tagged-object branches (a common OpenAI pattern), the generator emits an untagged enum so both forms deserialize:
// not #[serde(tag="type")]
Extensible enums (with Custom(String) fallback)
When the spec declares an anyOf of const strings plus an open string branch (or you opt in via [extensible_enums], see below), the enum has a Custom(String) arm so unknown values still deserialize:
Per-operation typed errors
Each operation has its own error enum that wraps the typed body of each documented response code:
let resp = client.create_response.await;
match resp
Streaming (SSE)
use *;
let streaming_config = StreamingConfig ;
Generated event types are tagged enums you can match on directly:
match ?
The generator also auto-detects streaming endpoints: any response declaring content: text/event-stream flips supports_streaming = true automatically. Explicit [[streaming.endpoints]] config still wins when present.
Spec-quirk overrides
Real specs lie. These TOML knobs let you patch quirks without forking the spec.
schema_extensions — overlay JSON/YAML fragments onto the spec
A list of files whose top-level objects are deep-merged into the main spec before analysis. Use it to add a text/event-stream content entry, add a missing operation, or tweak a schema without forking the upstream spec. Example: the Anthropic server example overlays sse-overlay.json so messages_post gets an SSE response variant.
[]
# Relative paths are resolved from the directory containing this TOML file.
= ["sse-overlay.json"]
nullable_overrides — force a field to Option<T>
When a spec marks a field as required + non-nullable but the API actually returns null. Format: "SchemaName.fieldName" = true.
[]
# OpenAI's spec uses a bare $ref for `error`; the API actually returns null on success.
= true
extensible_enums — force a closed enum to accept unknown values
When the spec declares a fixed enum but the API actually returns values outside the set (real-world drift). Renders the enum with a Custom(String) fallback variant. Accepts either the raw spec name or the rendered Rust type name.
[]
# CF spec declares lowercase ["apac", ..., "wnam"] but the API returns "WNAM".
= true
# OpenAI spec declares ["in-memory", "24h"] but the API returns "in_memory".
= true
type_mappings — override a primitive's Rust type
[]
= "chrono::DateTime<chrono::Utc>"
[generator.types] — typed-scalar strategy per format
Opt out of any individual typed scalar (e.g. fall back to String for date-times if you don't want chrono):
[]
= "string" # default: "chrono"
= "string" # default: "url"
= "string" # default: "bytes"
The CLI also supports --types-conservative, which collapses every typed scalar to String/i64/etc. Use it when you want zero optional-crate dependencies.
[generator.builders] — additive operation builders
Large operations can keep their existing flat async method and also expose a *_builder() entry point:
[]
= true
= 3
A builder is emitted when its optional query/header/body values and reachable optional request-body fields exceed threshold. Required path, query, header, and request-model values stay explicit. Setters own their values, and .send().await delegates to the existing flat method, so request serialization and response errors stay identical. Builder generation is disabled by default to preserve existing generated call sites.
OpenAPI 3.1 / 3.2 support
The generator accepts 3.0.x and 3.1.x specs; 3.2.x parses with an
experimental warning. Unknown fields are retained for compatibility, but an
unrecognized field may be ignored by analysis and code generation. Add a
focused fixture when relying on a less-common OpenAPI or JSON Schema keyword.
3.1 (JSON Schema 2020-12) keywords now modeled and read from typed fields:
| Keyword group | Status |
|---|---|
type as array (e.g. ["string", "null"]) |
typed + used for nullability |
prefixItems, unevaluatedItems, contains / minContains / maxContains |
typed |
patternProperties, propertyNames, unevaluatedProperties |
typed |
dependentRequired, dependentSchemas, if / then / else |
typed |
contentEncoding, contentMediaType, contentSchema |
typed |
$dynamicRef, $dynamicAnchor, $defs, $id, $schema, $comment |
typed (anchor-scope resolution is a follow-up) |
Path Item $ref resolution |
resolved at analysis time |
Webhooks (webhooks:) |
ingested as operations |
examples, example, title, deprecated, readOnly/writeOnly |
typed |
3.2 deltas (experimental):
| Delta | Status |
|---|---|
query HTTP method + PathItem.additionalOperations |
parses + emits client methods via reqwest::Method::from_bytes(...) |
OAuth deviceAuthorization flow + oauth2MetadataUrl |
typed |
Server.name, Tag.parent/kind/summary |
typed |
Discriminator.defaultMapping |
typed (captured; _Other(Value) fallback emission is a follow-up) |
MediaType.itemSchema, prefixEncoding, itemEncoding |
typed |
mediaTypes in Components |
typed |
Modeled Components family: Server, ServerVariable, SecurityScheme (apiKey / http / mutualTLS / oauth2 / openIdConnect with all flows), OAuthFlows, Encoding, Header, Example, Link, Callback, Tag, ExternalDocs, Discriminator. Modeling does not imply runtime support for every security or linking behavior; for example, mutual TLS credentials are not configured by generated clients.
The fixture harness under tests/conformance/ currently gates lossless L0
parsing; its later L1–L5 markers are tracked but deferred. The vendored JSON
Schema Test Suite runner enforces a no-regression ceiling for parse failures
and lossless round trips; it does not yet enforce semantic, feature-level pass
thresholds.
CLI
# Direct mode: client by default; use --types-only to omit it.
# Create a starter config, then generate with the config defaults.
# Explicit config mode.
# Server scope management — preserves TOML formatting (toml_edit)
Selectors are forgiving — operationId, METHOD /path, or tag:<name>. Typos surface Levenshtein-based "Did you mean …?" suggestions.
TOML reference
[]
= "openapi.json" # required
= "src/generated" # required
= "types" # informational label, not a directory
= [] # optional list of JSON/YAML overlays merged into the spec
[]
= false # generate SSE streaming client (requires [[streaming.endpoints]])
= true # generate HTTP REST client
= false # add specta::Type derives
= false # generate static operation registry (CLI/proxy routing)
= false # only generate the registry (skip types/client/streaming)
[]
= "https://api.example.com"
= 30 # 1-3600
[]
= 3 # 0-10
= 500 # 100-10000
= 16000 # 1000-300000
[]
= true
[]
= "Bearer" # Bearer | ApiKey | Custom (honored at runtime)
= "Authorization"
[[]]
= "content-type"
= "application/json"
[[]]
= "createChatCompletion"
= "chat/completions"
= "POST"
= "stream"
= "ChatCompletionStreamEvent"
= "text/event-stream"
[]
= "StartDeltaStop" # or "Continuous"
= ["response.created"]
= ["response.output_text.delta"]
= ["response.completed"]
[]
= "axum" # only axum supported today
= [ # selectors: operationId | "METHOD /path" | "tag:<name>"
"createResponse",
"POST /v1/messages",
"tag:Responses",
]
= false # drop schemas outside the combined selected
# client/server operation closure
[]
= [ # absent or empty means every client operation
"createResponse",
"GET /v1/models",
"tag:Files",
]
= false # opt in to the same union model pruning
[]
= true # see "Spec-quirk overrides" above
[]
= true # see "Spec-quirk overrides" above
[]
= "chrono::DateTime<chrono::Utc>"
[]
= "chrono" # chrono (default) | time | string
= "url" # url (default) | string
= "bytes" # bytes (default) | vec_u8 | string
= "uuid" # uuid (default) | string
= "base64" # base64 (default) | base64_url_unpadded | vec_u8 | string
= true # uint32/uint64 -> u32/u64
[]
= true
Testing
The compile tiers are intentionally different:
- Every pull request and push to
maingenerates all 54 supported OpenAPI specs, then compile-checks the Anthropic and OpenAI production specs against each generatedREQUIRED_DEPS.toml. - Weekly scheduled CI and manual workflow runs compile-check all 54 supported OpenAPI specs. The bundled Gitea Swagger 2.0 document is reported as skipped.
- Local
scripts/spec-compile.shruns the same full tier; pass spec names as arguments for a smaller targeted run.
Examples
# Library / generator examples
# End-to-end Axum server examples (generate + run)
Versioning
Until 1.0.0, a minor version may change generated Rust APIs when correcting output that was wrong or incomplete on the wire. Regenerating lets the compiler identify affected call sites. See the changelog for release history and migration notes.
Contributing
See CONTRIBUTING.md for setup, targeted test commands, snapshot guidance, and the generated-API compatibility checklist. Questions belong in GitHub Discussions; bugs and concrete feature requests use the repository issue forms.
By participating, you agree to the Code of Conduct. Security reports follow SECURITY.md; please do not file a public issue for a suspected vulnerability.
License
MIT