OpenAPI

OpenAPI is an open-source specification used to describe the interface design of RESTful APIs. It defines the details of the structure, parameters, return types, error codes, and other aspects of API requests and responses in JSON or YAML format, making communication between clients and servers more clear and standardized.

Initially developed as an open-source version of the Swagger specification, OpenAPI has now become an independent project and has gained support from many large enterprises and developers. Using the OpenAPI specification can help development teams collaborate better, reduce communication costs, and improve development efficiency. Additionally, OpenAPI provides developers with tools such as automatic API documentation generation, mock data, and test cases to facilitate development and testing work.

Salvo provides integration with OpenAPI (adapted from utoipa).

Example

use salvo::oapi::extract::*;
use salvo::prelude::*;

#[endpoint]
async fn hello(name: QueryParam<String, false>) -> String {
    format!("Hello, {}!", name.as_deref().unwrap_or("World"))
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt().init();

    let router = Router::new().push(Router::with_path("hello").get(hello));

    let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);

    let router = router
        .push(doc.into_router("/api-doc/openapi.json"))
        .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui"));

    let acceptor = TcpListener::new("127.0.0.1:5800").bind().await;
    Server::new(acceptor).serve(router).await;
}
[package]
name = "example-oapi-hello"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
salvo = { workspace = true, features = ["oapi"] }
tokio = { workspace = true, features = ["macros"] }
tracing.workspace = true
tracing-subscriber.workspace = true

To view the Swagger UI page, enter http://localhost:5800/swagger-uiopen in new window in your browser.

The OpenAPI integration in Salvo is quite elegant. For the example above, compared to a normal Salvo project, we only need to take the following steps:

  • Enable the oapi feature in Cargo.toml: salvo = { workspace = true, features = ["oapi"] };

  • Replace #[handler] with #[endpoint];

  • Use name: QueryParam<String, false> to retrieve the value of a query string. When you visit the URL http://localhost/hello?name=chris, the query string for this name parameter will be parsed. The false here indicates that this parameter is optional. If you visit http://localhost/hello without the name parameter, it will not cause an error. Conversely, if it is QueryParam<String, true>, it means that this parameter is required and an error will be returned if it is not provided.

  • Create an OpenAPI and create the corresponding Router. OpenApi::new("test api", "0.0.1").merge_router(&router) here merge_router means that this OpenAPI obtains the necessary documentation information by parsing a certain route and its descendant routes. Some routes may not provide information for generating documentation, and these routes will be ignored, such as Handler defined using the #[handler] macro instead of the #[endpoint] macro. In other words, in actual projects, for reasons such as development progress, you can choose not to generate OpenAPI documentation, or only partially generate it. Later, you can gradually increase the number of OpenAPI interfaces generated, and all you need to do is change the #[handler] to #[endpoint] and modify the function signature.

Extractors

By using use salvo::oapi::extract:*;, you can import commonly used data extractors that are pre-built in Salvo. These extractors provide necessary information to Salvo so that it can generate OpenAPI documentation.

  • QueryParam<T, const REQUIRED: bool>: an extractor that extracts data from query strings. QueryParam<T, false> means that this parameter is optional and can be omitted. QueryParam<T, true> means that this parameter is required and cannot be omitted. If it is not provided, an error will be returned;

  • HeaderParam<T, const REQUIRED: bool>: an extractor that extracts data from request headers. HeaderParam<T, false> means that this parameter is optional and can be omitted. HeaderParam<T, true> means that this parameter is required and cannot be omitted. If it is not provided, an error will be returned;

  • CookieParam<T, const REQUIRED: bool>: an extractor that extracts data from request cookies. CookieParam<T, false> means that this parameter is optional and can be omitted. CookieParam<T, true> means that this parameter is required and cannot be omitted. If it is not provided, an error will be returned;

  • PathParam<T>: an extractor that extracts path parameters from the request URL. If this parameter does not exist, the route matching will not be successful, so there is no case where it can be omitted;

  • FormBody<T>: an extractor that extracts information from submitted forms;

  • JsonBody<T>: an extractor that extracts information from JSON-formatted payloads submitted in requests.

#[endpoint]

When generating OpenAPI documentation, the #[endpoint] macro needs to be used instead of the regular #[handler] macro. It is actually an enhanced version of the #[handler] macro.

  • It can obtain the necessary information for generating OpenAPI documentation from the function signature.

  • For information that is not convenient to provide through the signature, it can be directly added as an attribute in the #[endpoint] macro. Information provided in this way will be merged with the information obtained through the function signature. If there is a conflict, the information provided in the attribute will overwrite the information provided through the function signature.

You can use the Rust's own #[deprecated] attribute on functions to mark it as deprecated and it will reflect to the generated OpenAPI spec. Only parameters has a special deprecated attribute to define them as deprecated.

#[deprecated] attribute supports adding additional details such as a reason and or since version but this is is not supported in OpenAPI. OpenAPI has only a boolean flag to determine deprecation. While it is totally okay to declare deprecated with reason #[deprecated = "There is better way to do this"] the reason would not render in OpenAPI spec.

Doc comment at decorated function will be used for description and summary of the path. First line of the doc comment will be used as the summary and the whole doc comment will be used as description.

/// This is a summary of the operation
///
/// All lines of the doc comment will be included to operation description.
#[endpoint]
fn endpoint() {}

ToParameters

Generate [path parameters][path_params] from struct's fields.

This is #[derive] implementation for ToParameters trait.

Typically path parameters need to be defined within [#[salvo_oapi::endpoint(...parameters(...))]][path_params] section for the endpoint. But this trait eliminates the need for that when structopen in new windows are used to define parameters. Still [std::primitive] and String path parameters or [tuple] style path parameters need to be defined within parameters(...) section if description or other than default configuration need to be given.

You can use the Rust's own #[deprecated] attribute on field to mark it as deprecated and it will reflect to the generated OpenAPI spec.

#[deprecated] attribute supports adding additional details such as a reason and or since version but this is is not supported in OpenAPI. OpenAPI has only a boolean flag to determine deprecation. While it is totally okay to declare deprecated with reason #[deprecated = "There is better way to do this"] the reason would not render in OpenAPI spec.

Doc comment on struct fields will be used as description for the generated parameters.

#[derive(salvo_oapi::ToParameters, serde::Deserialize)]
struct Query {
    /// Query todo items by name.
    name: String
}

ToParameters Container Attributes for #[salvo(parameters(...))]

The following attributes are available for use in on the container attribute #[salvo(parameters(...))] for the struct deriving ToParameters:

  • names(...) Define comma separated list of names for unnamed fields of struct used as a path parameter. Only supported on unnamed structs.
  • style = ... Defines how all parameters are serialized by ParameterStyle. Default values are based on parameter_in attribute.
  • default_parameter_in = ... = Defines default where the parameters of this field are used with a value from parameter::ParameterIn. If this attribute is not supplied, then the default value is from query.
  • rename_all = ... Can be provided to alternatively to the serde's rename_all attribute. Effectively provides same functionality.

Use names to define name for single unnamed argument.

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(names("id")))]
struct Id(u64);

Use names to define names for multiple unnamed arguments.

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(names("id", "name")))]
struct IdAndName(u64, String);

ToParameters Field Attributes for #[salvo(parameter(...))]

The following attributes are available for use in the #[salvo(parameter(...))] on struct fields:

  • style = ... Defines how the parameter is serialized by ParameterStyle. Default values are based on parameter_in attribute.

  • parameter_in = ... = Defines where the parameters of this field are used with a value from parameter::ParameterIn. If this attribute is not supplied, then the default value is from query.

  • explode Defines whether new parameter=value pair is created for each parameter within object or array.

  • allow_reserved Defines whether reserved characters :/?#[]@!$&'()*+,;= is allowed within value.

  • example = ... Can be method reference or json!(...). Given example will override any example in underlying parameter type.

  • value_type = ... Can be used to override default type derived from type of the field used in OpenAPI spec. This is useful in cases where the default type does not correspond to the actual type e.g. when any third-party types are used which are not ToSchemas nor primitive typesopen in new window. Value can be any Rust type what normally could be used to serialize to JSON or custom type such as Object. Object will be rendered as generic OpenAPI object.

  • inline If set, the schema for this field's type needs to be a ToSchema, and the schema definition will be inlined.

  • default = ... Can be method reference or json!(...).

  • format = ... May either be variant of the KnownFormat enum, or otherwise an open value as a string. By default the format is derived from the type of the property according OpenApi spec.

  • write_only Defines property is only used in write operations POST,PUT,PATCH but not in GET

  • read_only Defines property is only used in read operations GET but not in POST,PUT,PATCH

  • nullable Defines property is nullable (note this is different to non-required).

  • required = ... Can be used to enforce required status for the parameter. [See rules][derive@ToParameters#field-nullability-and-required-rules]

  • rename = ... Can be provided to alternatively to the serde's rename attribute. Effectively provides same functionality.

  • multiple_of = ... Can be used to define multiplier for a value. Value is considered valid division will result an integer. Value must be strictly above 0.

  • maximum = ... Can be used to define inclusive upper bound to a number value.

  • minimum = ... Can be used to define inclusive lower bound to a number value.

  • exclusive_maximum = ... Can be used to define exclusive upper bound to a number value.

  • exclusive_minimum = ... Can be used to define exclusive lower bound to a number value.

  • max_length = ... Can be used to define maximum length for string types.

  • min_length = ... Can be used to define minimum length for string types.

  • pattern = ... Can be used to define valid regular expression in ECMA-262 dialect the field value must match.

  • max_items = ... Can be used to define maximum items allowed for array fields. Value must be non-negative integer.

  • min_items = ... Can be used to define minimum items allowed for array fields. Value must be non-negative integer.

  • with_schema = ... Use schema created by provided function reference instead of the default derived schema. The function must match to fn() -> Into<RefOr<Schema>>. It does not accept arguments and must return anything that can be converted into RefOr<Schema>.

  • additional_properties = ... Can be used to define free form types for maps such as HashMap and BTreeMap. Free form type enables use of arbitrary types within map values. Supports formats additional_properties and additional_properties = true.

Field nullability and required rules

Same rules for nullability and required status apply for ToParameters field attributes as for

ToSchema field attributes. [See the rules][derive@ToSchema#field-nullability-and-required-rules].

Partial #[serde(...)] attributes support

ToParameters derive has partial support for serde attributesopen in new window. These supported attributes will reflect to the generated OpenAPI doc. The following attributes are currently supported:

  • rename_all = "..." Supported at the container level.
  • rename = "..." Supported only at the field level.
  • default Supported at the container level and field level according to serde attributesopen in new window.
  • skip_serializing_if = "..." Supported only at the field level.
  • with = ... Supported only at field level.
  • skip_serializing = "..." Supported only at the field or variant level.
  • skip_deserializing = "..." Supported only at the field or variant level.
  • skip = "..." Supported only at the field level.

Other serde attributes will impact the serialization but will not be reflected on the generated OpenAPI doc.

Examples

Demonstrate ToParameters usage with the #[salvo(parameters(...))] container attribute to be used as a path query, and inlining a schema 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) {
    // ...
}

Override String with i64 using value_type attribute.

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
    #[salvo(parameter(value_type = i64))]
    id: String,
}

Override String with Object using value_type attribute. Object will render as type: object in OpenAPI spec.

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
    #[salvo(parameter(value_type = Object))]
    id: String,
}

You can use a generic type to override the default type of the field.

# use salvo_oapi::ToParameters;

#[derive(ToParameters, serde::Deserialize)]
#[salvo(parameters(default_parameter_in = Query))]
struct Filter {
    #[salvo(parameter(value_type = Option<String>))]
    id: String
}

You can even override a [Vec] with another one.

# 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>
}

We can override value 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
}

Example with validation attributes.

#[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>,
}

Use schema_with to manually implement schema for a field.

# 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 = ...: 支持于 serde 类似的语法定义重命名字段的规则. 如果同时定义了 #[serde(rename_all = "...")]#[salvo(schema(rename_all = "..."))], 则优先使用 #[serde(rename_all = "...")].

  • symbol = ...: 一个字符串字面量, 用于定义结构在 OpenAPI 中线上的名字路径. 比如 #[salvo(schema(symbol = "path.to.Pet"))].

  • default: Can be used to populate default values on all fields using the struct’s Default implementation.

Error handling

For general applications, we will define a global error type (AppError), and implement Writer or Scribe for AppError, so that errors can be sent to the client as web page information.

For OpenAPI, in order to achieve the necessary error message, we also need to implement EndpointOutRegister for this error:

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 error centrally defines all error messages that may be returned by the entire web application. However, in many cases, our Handler may only contain a few specific error types. At this time, status_codes can be used to filter out the required error type information:

#[endpoint(status_codes(201, 409))]
pub async fn create_todo(new_todo: JsonBody<Todo>) -> Result<StatusCode, Error> {
     Ok(StatusCode::CREATED)
}