import { Tab, Tabs } from 'rspress/theme';
import OapiHelloCode from '../../../../codes_md/oapi-hello/src/main.mdx';
import OapiHelloCargoCode from '../../../../codes_md/oapi-hello/Cargo.mdx';
# OpenAPI Documentation Generation
OpenAPI is an open-source specification for describing RESTful API interface designs. It defines API request/response structures, parameters, return types, error codes, and other details in JSON or YAML format, making communication between clients and servers more explicit and standardized.
Originally the open-source version of the Swagger specification, OpenAPI has now become an independent project with support from many large enterprises and developers. Using the OpenAPI specification helps development teams collaborate better, reduce communication overhead, and improve efficiency. Additionally, OpenAPI provides tools for automatically generating API documentation, mock data, and test cases, facilitating development and testing workflows.
Salvo offers OpenAPI integration (adapted from [utoipa](https://github.com/juhaku/utoipa)). Leveraging its own features, Salvo elegantly extracts relevant OpenAPI data type information automatically from `Handler`. It also integrates several popular open-source OpenAPI interfaces like SwaggerUI, scalar, rapidoc, and redoc.
To address Rust's long type names that may not be suitable for OpenAPI usage, `salvo-oapi` provides the `Namer` type, allowing customization of rules to modify type names in OpenAPI.
_**Example Code**_
<Tabs>
<Tab label="main.rs">
<OapiHelloCode/>
</Tab>
<Tab label="Cargo.toml">
<OapiHelloCargoCode/>
</Tab>
</Tabs>
Enter `http://localhost:5800/swagger-ui` in your browser to view the Swagger UI page.
Salvo's OpenAPI integration is remarkably elegant. For the example above, compared to a regular Salvo project, we only needed to:
- Enable the `oapi` feature in `Cargo.toml`: `salvo = { workspace = true, features = ["oapi"] }`;
- Replace `#[handler]` with `#[endpoint]`;
- Use `name: QueryParam<String, false>` to get query string values. When visiting `http://localhost/hello?name=chris`, this `name` query string will be parsed. Here, `false` indicates the parameter is optional—accessing `http://localhost/hello` won't trigger an error. Conversely, `QueryParam<String, true>` means the parameter is mandatory, and its absence will return an error.
- Create `OpenAPI` and its corresponding `Router`. `OpenApi::new("test api", "0.0.1").merge_router(&router)` uses `merge_router` to indicate that this `OpenAPI` extracts necessary documentation information by parsing a specific route and its descendants. Routes whose `Handler` doesn't provide documentation generation info will be ignored (e.g., those defined with `#[handler]` instead of `#[endpoint]`). In real projects, you can choose not to generate OpenAPI docs (or generate them partially) for development speed. Later, you can incrementally add OpenAPI interfaces by simply changing `#[handler]` to `#[endpoint]` and adjusting function signatures.
## Data Extractors
Import predefined common data extractors via `use salvo::oapi::extract::*;`. Extractors provide Salvo with essential information for OpenAPI documentation generation.
- `QueryParam<T, const REQUIRED: bool>`: Extracts data from query strings. `QueryParam<T, false>` means the parameter is optional, while `QueryParam<T, true>` makes it mandatory (returns an error if missing).
- `HeaderParam<T, const REQUIRED: bool>`: Extracts data from request headers. `HeaderParam<T, false>` indicates optionality; `HeaderParam<T, true>` enforces requirement.
- `CookieParam<T, const REQUIRED: bool>`: Extracts data from request cookies. `CookieParam<T, false>` allows omission; `CookieParam<T, true>` requires presence.
- `PathParam<T>`: Extracts path parameters from URLs. Absence prevents route matching, making this inherently non-optional.
- `FormBody<T>`: Extracts information from form submissions.
- `JsonBody<T>`: Extracts information from JSON payloads.
## `#[endpoint]`
When generating OpenAPI docs, use the `#[endpoint]` macro instead of the regular `#[handler]` macro—it's an enhanced version.
- It gathers OpenAPI-required information from function signatures.
- For details inconvenient to provide via signatures, add attributes directly in `#[endpoint]`. These merge with signature-derived info, with attribute values overriding any conflicts.
You can mark a Handler as deprecated using Rust's built-in `#[deprecated]`. While this attribute supports reasons/versioning, OpenAPI ignores such details, only recognizing deprecation status.
Code documentation comments auto-populate OpenAPI: the first line becomes the _`summary`_, while the entire comment forms the _`description`_.
```rust
/// This is a summary of the operation
///
/// All lines of the doc comment will be included to operation description.
#[endpoint]
fn endpoint() {}
ToSchema
Define data structures with #[derive(ToSchema)]
:
#[derive(ToSchema)]
struct Pet {
id: u64,
name: String,
}
Optional settings via #[salvo(schema(...))]
:
ToParameters
Generates [path parameters][path_parameters] from struct fields.
This is the #[derive]
implementation of the ToParameters
trait.
Normally, path parameters need defining in the endpoint's [#[salvo_oapi::endpoint(...parameters(...))]
][path_parameters]. But when using [struct
][struct] for parameters, this step is unnecessary. However, descriptions or default configuration changes still require defining [primitive types
][primitive] and [String
][std_string] path parameters or [tuple]-style ones in parameters(...)
.
Mark fields as deprecated with Rust's #[deprecated]
, reflected in the generated OpenAPI spec.
While #[deprecated]
supports reasons/versioning, OpenAPI only acknowledges boolean deprecation status. Reasons like #[deprecated = "There is better way to do this"]
won't appear in OpenAPI.
Field doc comments become parameter descriptions in OpenAPI.
#[derive(salvo_oapi::ToParameters, serde::Deserialize)]
struct Query {
/// Query todo items by name.
name: String
}
ToParameters Container Attributes for#[salvo(parameters(...))]
These attributes apply to structs deriving ToParameters
via #[salvo(parameters(…))]
:
names(...)
: Defines comma-separated names for unnamed struct fields used as path parameters. Only for unnamed structs.
style = ...
: Defines serialization for all parameters via [ParameterStyle
][style]. Defaults based on parameter_in
.
default_parameter_in = ...
: Sets default parameter location from [parameter::ParameterIn
][in_enum]. Defaults to query
if unspecified.
rename_all = ...
: Alternative to serde
's rename_all
with identical functionality.
Use names
to name single unnamed parameters:
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(names("id")))]
struct Id(u64);
Or multiple:
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(names("id", "name")))]
struct IdAndName(u64, String);
ToParameters Field Attributes for#[salvo(parameter(...))]
Field-level attributes via #[salvo(parameter(...))]
:
style = ...
: Serialization via [ParameterStyle
][style]. Defaults based on parameter_in
.
parameter_in = ...
: Parameter location from [parameter::ParameterIn
][in_enum]. Defaults to query
.
explode
: Creates separate parameter=value
pairs for each object
/array
item.
allow_reserved
: Permits reserved characters :/?#[]@!$&'()*+,;=
in values.
example = ...
: Method reference or json!(...)
. Overrides underlying type's example.
value_type = ...
: Overrides default OpenAPI field type. Useful for third-party types not implementing ToSchema
or [primitive
types][primitive]. Accepts any JSON-serializable Rust type or custom types like Object
.
inline
: Requires field type to implement ToSchema
, inlining its definition.
default = ...
: Method reference or json!(...)
.
format = ...
: KnownFormat
variant or open string value. Defaults derived from type per OpenAPI spec.
write_only
: Restricts field to write operations (POST,PUT,PATCH).
read_only
: Restricts field to read operations (GET).
nullable
: Indicates if field can be null
(distinct from optionality).
required = ...
: Enforces mandatory parameters. See rules.
rename = ...
: Alternative to serde
's rename
.
multiple_of = ...
: Value must divide evenly by this (>0).
maximum = ...
: Inclusive upper bound.
minimum = ...
: Inclusive lower bound.
exclusive_maximum = ...
: Exclusive upper bound.
exclusive_minimum = ...
: Exclusive lower bound.
max_length = ...
: Maximum string
length.
min_length = ...
: Minimum string
length.
pattern = ...
: ECMA-262 regex for valid values.
max_items = ...
: Maximum array
items (non-negative).
min_items = ...
: Minimum array
items (non-negative).
with_schema = ...
: Uses function-generated schema
instead of default. Function must satisfy fn() -> Into<RefOr<Schema>>
.
additional_properties = ...
: Defines free-form types for map
s (e.g., HashMap
/BTreeMap
). Formats: additional_properties
or additional_properties = true
.
Field nullability and required rules
Rules for ToParameters
field attributes mirror those for ToSchema
. See rules.
Partial#[serde(...)]
attributes support
ToParameters
derivation supports select [serde attributes][serde attributes], reflected in OpenAPI docs:
rename_all = "..."
(container-level)
rename = "..."
(field-level only)
default
(container/field-level per [serde][serde attributes])
skip_serializing_if = "..."
(field-level only)
with = ...
(field-level only)
skip_serializing = "..."
(field/variant-level)
skip_deserializing = "..."
(field/variant-level)
skip = "..."
(field-level only)
Other serde
attributes affect serialization but not OpenAPI generation.
Examples
Demonstrates #[salvo(parameters(...))]
container attributes with ToParameters
for path parameters, inlining a query field:
use serde::Deserialize;
use salvo_core::prelude::*;
use salvo_oapi::{ToParameters, ToSchema};
#[derive(Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
enum PetKind {
Dog,
Cat,
}
#[derive(Deserialize, ToParameters)]
struct PetQuery {
/// Name of pet
name: Option<String>,
/// Age of pet
age: Option<i32>,
/// Kind of pet
#[salvo(parameter(inline))]
kind: PetKind
}
#[salvo_oapi::endpoint(
parameters(PetQuery),
responses(
(status_code = 200, description = "success response")
)
)]
async fn get_pet(query: PetQuery) {
// ...
}
Overriding String
with i64
via value_type
:
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
#[salvo(parameter(value_type = i64))]
id: String,
}
Overriding String
with Object
(renders as type:object
in OpenAPI):
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
#[salvo(parameter(value_type = Object))]
id: String,
}
Override with generics:
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
#[salvo(parameter(value_type = Option<String>))]
id: String
}
Override Vec
with another Vec
:
# use salvo_oapi::ToParameters;
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
#[salvo(parameter(value_type = Vec<i32>))]
id: Vec<String>
}
Override with another ToSchema
:
# use salvo_oapi::{ToParameters, ToSchema};
#[derive(ToSchema)]
struct Id {
value: i64,
}
#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
#[salvo(parameter(value_type = Id))]
id: String
}
Validation examples:
#[derive(salvo_oapi::ToParameters, serde::Deserialize)]
struct Item {
#[salvo(parameter(maximum = 10, minimum = 5, multiple_of = 2.5))]
id: i32,
#[salvo(parameter(max_length = 10, min_length = 5, pattern = "[a-z]*"))]
value: String,
#[salvo(parameter(max_items = 5, min_items = 1))]
items: Vec<String>,
}
Manual schema implementation via schema_with
:
# use salvo_oapi::schema::Object;
fn custom_type() -> Object {
Object::new()
.schema_type(salvo_oapi::SchemaType::String)
.format(salvo_oapi::SchemaFormat::Custom(
"email".to_string(),
))
.description("this is the description")
}
#[derive(salvo_oapi::ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Query {
#[salvo(parameter(schema_with = custom_type))]
email: String,
}
-
rename_all = ...
: Supports serde
-like syntax for renaming rules. If both #[serde(rename_all = "...")]
and #[salvo(schema(rename_all = "..."))]
exist, #[serde(rename_all = "...")]
takes precedence.
-
symbol = ...
: String literal defining the struct's OpenAPI schema path (e.g., #[salvo(schema(symbol = "path.to.Pet"))]
).
-
default
: Uses the struct's Default
impl to populate field defaults.
Error Handling Approach
For typical applications, we define a global error type (AppError), implementing Writer
or Scribe
to send errors as web messages to clients.
For OpenAPI, we additionally implement EndpointOutRegister
to expose error details:
use salvo::http::{StatusCode, StatusError};
use salvo::oapi::{self, EndpointOutRegister, ToSchema};
impl EndpointOutRegister for Error {
fn register(components: &mut oapi::Components, operation: &mut oapi::Operation) {
operation.responses.insert(
StatusCode::INTERNAL_SERVER_ERROR.as_str(),
oapi::Response::new("Internal server error").add_content("application/json", StatusError::to_schema(components)),
);
operation.responses.insert(
StatusCode::NOT_FOUND.as_str(),
oapi::Response::new("Not found").add_content("application/json", StatusError::to_schema(components)),
);
operation.responses.insert(
StatusCode::BAD_REQUEST.as_str(),
oapi::Response::new("Bad request").add_content("application/json", StatusError::to_schema(components)),
);
}
}
This centralizes all possible application errors. However, individual Handler
s may only return specific subsets. Use status_codes
to filter relevant errors:
#[endpoint(status_codes(201, 409))]
pub async fn create_todo(new_todo: JsonBody<Todo>) -> Result<StatusCode, Error> {
Ok(StatusCode::CREATED)
}