A Rust procedural macro crate that generates type-safe, async HTTP clients from OpenAPI 3.0 specifications.
- 🚀 Zero-runtime dependencies - Pure compile-time code generation
- 🔒 Type-safe - Full Rust type system integration with proper error handling
- 📚 Auto-documented - Generates comprehensive documentation from OpenAPI descriptions
- 🛡️ Keyword-safe - Handles Rust keywords automatically with proper escaping
- ⚡ Async/await - Built on
reqwestwith full async support - 🎯 Easy to use - Simple macro interface with sensible defaults
- 🔧 Flexible - Supports both JSON and YAML OpenAPI specifications
Add this to your Cargo.toml:
[dependencies]
openapi-gen = "0.3"
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"use openapi_gen::openapi_client;
// Generate client from OpenAPI spec with auto-generated name
openapi_client!("path/to/your/openapi.json");
// Or specify a custom client name
openapi_client!("path/to/your/openapi.json", "MyApiClient");
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client instance
let client = MyApiClient::new("https://api.example.com");
// Use the generated methods
let users = client.list_users(Some(10), None, None).await?;
let user = client.get_user_by_id(123).await?;
Ok(())
}The macro generates:
/// Represents a user in the system with comprehensive profile information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
/// Unique identifier for the user
pub id: i64,
/// User's chosen username
pub username: String,
/// User's email address
pub email: String,
/// User type (tests Rust keyword as field name)
pub r#type: String,
// ... more fields
}impl MyApiClient {
/// List all users
///
/// Retrieve a paginated list of all users in the system. Supports filtering and sorting.
///
/// **HTTP Method:** `GET`
/// **Path:** `/users`
/// **Operation ID:** `listUsers`
pub async fn list_users(
&self,
limit: Option<i32>,
offset: Option<i64>,
r#type: Option<String>
) -> ApiResult<UserList> {
// Generated implementation
}
}The generated client includes:
- API information from the OpenAPI
infosection - Method documentation from operation summaries and descriptions
- Type documentation from schema descriptions
- Field documentation from property descriptions
| Feature | Support | Notes |
|---|---|---|
| HTTP Methods | ✅ | GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE |
| Path Parameters | ✅ | Automatic URL template substitution |
| Query Parameters | ✅ | Optional and required parameters |
| Request Bodies | ✅ | JSON request bodies |
| Response Types | ✅ | Typed response parsing |
| Schema References | ✅ | $ref resolution for reusable components |
| Enums | ✅ | String enumerations with proper Rust enum generation |
| Arrays | ✅ | Vec<T> generation for array types |
| Objects | ✅ | Struct generation with proper field types |
| Optional Fields | ✅ | Option<T> for non-required fields |
| Nested Objects | ✅ | Complex object hierarchies |
| Type Aliases | ✅ | Simple type aliases |
| Rust Keywords | ✅ | Automatic escaping with r# or _ suffix |
The generator automatically handles Rust keywords in:
- Field names:
type→r#type,self→self_ - Parameter names:
const→r#const - Method names: Derived from operation IDs with keyword escaping
Special handling for self and Self (cannot be raw identifiers):
self→self_Self→Self_
The generated client includes a comprehensive error type:
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("API error {status}: {message}")]
Api { status: u16, message: String },
}
pub type ApiResult<T> = Result<T, ApiError>;// Basic client
let client = MyApiClient::new("https://api.example.com");
// Client with custom HTTP client
let http_client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()?;
let client = MyApiClient::with_client("https://api.example.com", http_client);The crate supports reqwest-middleware for advanced use cases like request
signing, retries, and logging. Enable the middleware feature in your
Cargo.toml:
[!NOTE] The
jsonfeature is required forreqwest-middlewarewhen your API includes operations with request bodies (POST, PUT, PATCH), which is common in most APIs.
[dependencies]
openapi-gen = { version = "0.3", features = ["middleware"] }
reqwest-middleware = { version = "0.4", features = ["json"] }
reqwest-retry = "0.7" # Optional, for retry middlewareExample using middleware:
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
// Create a client with retry middleware
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
let middleware_client = ClientBuilder::new(reqwest::Client::new())
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build();
// Use with generated client (same as regular reqwest::Client)
let api = MyApiClient::with_client("https://api.example.com", middleware_client);This enables use cases like:
- Request signing (e.g., for biscuit tokens)
- Automatic retries with exponential backoff
- Request/response logging
- Custom authentication flows
- Rate limiting
The crate supports synchronous/blocking HTTP clients via the blocking feature
flag. Enable it in your Cargo.toml:
[dependencies]
openapi-gen = { version = "0.3", features = ["blocking"] }Example using blocking client:
// Create a blocking client
let blocking_client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()?;
// Use with generated client (same method names, but synchronous)
let api = MyApiClient::with_client("https://api.example.com", blocking_client);
// Methods are synchronous - no .await needed
let user = api.get_user(123)?;
let users = api.list_users(Some(10), Some(0))?;Key differences:
- Methods are synchronous (
fninstead ofasync fn) - No
.awaitneeded on method calls - Same method names and signatures as async versions
- Compatible with
reqwest::blocking::Client
OpenAPI parameters are mapped to Rust function parameters following OpenAPI 3.0 specification rules:
- Path parameters: Always required (no
Optionwrapper) - Query/Header/Cookie parameters: Optional by default, wrapped in
Option<T>unless markedrequired: true
// Path parameters are always required
let user = client.get_user_by_id(123).await?;
// Query parameters are optional by default
let users = client.list_users(Some(10), Some(0), Some("admin")).await?;
let all_users = client.list_users(None, None, None).await?;String parameters use &str for better ergonomics:
// String parameters accept &str (not String)
let user = client.get_user_by_id(123).await?;
let comments = client.get_post_comments("post123", Some(true)).await?;
let filtered_users = client.list_users(None, None, Some("admin")).await?;The openapi_client! macro supports several configuration options to customize
the generated code:
Generate dedicated parameter structs for operations instead of individual parameters:
// Enable parameter structs
openapi_client!("openapi.json", use_param_structs = true);
openapi_client!("openapi.json", "MyApiClient", use_param_structs = true);This generates dedicated parameter structs for each operation:
// Instead of multiple parameters:
// client.list_users(Some(10), Some(0), Some("admin"), None, None).await?
// Use parameter structs with fluent API:
let params = ListUsersParams::new()
.with_limit(10)
.with_offset(0)
.with_type("admin");
let users = client.list_users(params).await?;
// Or use Default for all optional parameters (when no required params):
let users = client.list_users(ListUsersParams::default()).await?;
// Required parameters are passed to new():
let params = GetUserByIdParams::new(123); // Required path parameter
let user = client.get_user_by_id(params).await?;
// Mix required and optional parameters:
let params = GetPostCommentsParams::new("post-123") // Required param
.with_self_(true); // Optional param
let comments = client.get_post_comments(params).await?;Benefits of parameter structs:
- Cleaner code - No need to pass multiple
Nonevalues - Named parameters - Clear what each value represents
- Fluent API - Chain
with_*methods to set only the parameters you need - Type safety - Required parameters enforced at compile time
- Future-proof - Adding new optional parameters won't break existing code
When to use:
- Operations with 3+ parameters
- APIs that frequently add new optional parameters
- When you want more readable client code
Add custom attributes to all generated structs:
// Add PartialEq to all generated structs
openapi_client!("openapi.json", struct_attrs = (derive(PartialEq)));
// Add multiple derives
openapi_client!("openapi.json", struct_attrs = (derive(PartialEq, Hash)));
// Combine with custom client name
openapi_client!("openapi.json", "MyApi", struct_attrs = (derive(PartialEq)));
// Add multiple types of attributes
openapi_client!(
"openapi.json",
struct_attrs = (derive(PartialEq, Eq), cfg(feature = "extra"))
);
// Combine with parameter structs
openapi_client!(
"openapi.json",
"MyApi",
use_param_structs = true,
struct_attrs = (derive(PartialEq))
);The struct_attrs option:
- Applies to all generated structs (data types, enums, and parameter structs)
- Adds attributes in addition to the default
#[derive(Debug, Clone, Serialize, Deserialize)] - Supports any valid Rust attribute syntax
Common use cases:
- Adding
PartialEqfor comparison operations - Adding
Hashfor using structs as HashMap keys - Adding
Eq,PartialOrd,Ordfor sorting - Adding conditional compilation with
cfgattributes - Adding custom derives from third-party crates
[!NOTE] The default derives (
Debug,Clone,Serialize,Deserialize) are always included as they are required for the client to function properly.
use openapi_gen::openapi_client;
use serde_json::json;
// Generate the client
openapi_client!("openapi.json", "TestApi");
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = TestApi::new("https://api.test.com/v2");
// List users with pagination
let users = client.list_users(Some(20), Some(0), None).await?;
println!("Found {} users", users.total);
// Create a new user
let new_user = json!({
"username": "john_doe",
"email": "john@example.com",
"firstName": "John",
"lastName": "Doe"
});
let created_user = client.create_user(new_user).await?;
// Get user details
let user = client.get_user_by_id(created_user.id).await?;
println!("User: {} <{}>", user.username, user.email);
// Update user
let update_data = json!({
"firstName": "Jonathan"
});
let updated_user = client.update_user(user.id, update_data).await?;
Ok(())
}- Rust: 2024 edition or later
- OpenAPI: 3.0.x specifications (JSON or YAML)
Runtime dependencies (required in your project):
reqwest- HTTP client with JSON supportserde- Serialization framework (with derive feature)serde_json- JSON serializationthiserror- Error handlingtokio- Async runtimereqwest-middleware- Middleware support (only required withmiddlewarefeature)
cargo buildcargo testcargo doc --no-deps --openThe project includes a comprehensive test OpenAPI specification that validates all crate features.
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.