OpenAPI Documentation Generation
OpenAPI is an open-source specification for describing RESTful API interface designs. It defines API request and response structures, parameters, return types, error codes, and other details in JSON or YAML format, making communication between client and server more explicit and standardized.
OpenAPI was originally the open-source version of the Swagger specification and has now become an independent project supported by many large enterprises and developers. Using the OpenAPI specification helps development teams collaborate better, reduce communication costs, and improve development efficiency. Additionally, OpenAPI provides developers with tools for automatically generating API documentation, mock data, and test cases, facilitating development and testing work.
Salvo provides OpenAPI integration (modified from utoipa). Salvo elegantly extracts relevant OpenAPI data type information automatically from Handler based on its own characteristics. Salvo also integrates several popular open-source OpenAPI interfaces such as SwaggerUI, Scalar, RapiDoc, and ReDoc.
Since Rust type names can be long and not always suitable for OpenAPI usage, salvo-oapi provides the Namer type, which allows customizing rules to change type names in OpenAPI as needed.
Example Code
Enter http://localhost:8698/swagger-ui in your browser to see the Swagger UI page.
The OpenAPI integration in Salvo is quite elegant. For the example above, compared to a normal Salvo project, we just did the following steps:
-
Enable the
oapifeature inCargo.toml:salvo = { workspace = true, features = ["oapi"] }; -
Replace
#[handler]with#[endpoint]; -
Use
name: QueryParam<String, false>to get the value of the query string. When you visithttp://localhost/hello?name=chris, thenamequery string will be parsed. ThefalseinQueryParam<String, false>means that this parameter is optional. If you visithttp://localhost/hello, it will not report an error. On the contrary, if it isQueryParam<String, true>, it means that this parameter must be provided, otherwise an error will be returned. -
Create
OpenAPIand the correspondingRouter. Themerge_routerinOpenApi::new("test api", "0.0.1").merge_router(&router)means that thisOpenAPIobtains the necessary document information by parsing a certain route and its sub-routes. SomeHandlers of routes may not provide information for generating documents, and these routes will be ignored, such asHandlers defined using the#[handler]macro instead of the#[endpoint]macro. That is to say, in actual projects, for reasons such as development progress, you can choose not to generate OpenAPI documents, or partially generate OpenAPI documents. Subsequently, you can gradually increase the number of OpenAPI interfaces generated, and all you need to do is change#[handler]to#[endpoint]and modify the function signature.
Data Extractors
You can import preset common data extractors through use salvo::oapi::extract::*;. The extractor will provide some necessary information to Salvo so that Salvo can generate OpenAPI documents.
-
QueryParam<T, const REQUIRED: bool>: An extractor for extracting data from query strings.QueryParam<T, false>means that this parameter is not required 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 for extracting data from the request header.HeaderParam<T, false>means that this parameter is not required 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 for extracting data from the request cookie.CookieParam<T, false>means that this parameter is not required 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 for extracting 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>: Extracts information from the submitted form of the request; -
JsonBody<T>: Extracts information from the JSON-formatted payload submitted by the request;
#[endpoint]
When generating OpenAPI documents, you need to use the #[endpoint] macro 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 through the function signature;
-
For information that is not convenient to provide through the signature, you can directly provide it by adding attributes in the
#[endpoint]macro. The information provided in this way will be merged with the information obtained through the function signature. If there is a conflict, it will overwrite the information provided by the function signature.
You can use Rust's built-in #[deprecated] attribute to mark a Handler as obsolete. Although the #[deprecated] attribute supports adding information such as the reason for deprecation and the version, OpenAPI does not support it, so this information will be ignored when generating OpenAPI.
The doc comment part in the code will be automatically extracted to generate OpenAPI. The first line is used to generate summary, and the entire comment part will be used to generate description.
ToSchema
You can use #[derive(ToSchema)] to define data structures:
You can use #[salvo(schema(...))] to define optional settings:
-
example = ...can bejson!(...).json!(...)will be parsed byserde_json::json!asserde_json::Value. -
xml(...)can be used to define Xml object properties:
ToParameters
Generate path parameters from the fields of the structure.
This is the #[derive] implementation of the ToParameters trait.
Usually, path parameters need to be defined in #[salvo_oapi::endpoint(...parameters(...))] of the endpoint. However, when using a struct to define parameters, the above steps can be omitted. Nevertheless, if you need to give a description or change the default configuration, then primitive types and String path parameters or [tuple] style path parameters still need to be defined in parameters(...).
You can use Rust's built-in #[deprecated] attribute to mark fields as deprecated, which will be reflected in the generated OpenAPI specification.
The #[deprecated] attribute supports adding extra information such as the reason for deprecation or the version from which it is deprecated, but OpenAPI does not support it. OpenAPI only supports a boolean value to determine whether it is deprecated. Although it is perfectly possible to declare a deprecation with a reason, such as #[deprecated = "There is better way to do this"], this reason will not be presented in the OpenAPI specification.
The comment document on the structure field will be used as the parameter description in the generated OpenAPI specification.
ToParameters Container Attributes for #[salvo(parameters(...))]
The following attributes can be used in the container attribute #[salvo(parameters(…))] of the structure derived from ToParameters
names(...)Defines a comma-separated list of names for the unnamed fields of the structure used as path parameters. Only supported on unnamed structures.style = ...Defines the serialization method for all parameters, specified byParameterStyle. The default value is based on theparameter_inattribute.default_parameter_in = ...Defines the default position used by the parameters of this field. The value of this position comes fromparameter::ParameterIn. If this attribute is not provided, the default is fromquery.rename_all = ...can be used as an alternative toserde'srename_all. It actually provides the same functionality.
Use names to define a name for a single unnamed parameter.
Use names to define names for multiple unnamed parameters.
ToParameters Field Attributes for #[salvo(parameter(...))]
The following attributes can be used on structure fields #[salvo(parameter(...))]:
-
style = ...Defines how parameters are serialized byParameterStyle. The default value is based on theparameter_inattribute. -
parameter_in = ...Use the value fromparameter::ParameterInto define where this field parameter is. If this value is not provided, the default is fromquery. -
explodeDefines whether to create a newparameter=valuepair for each parameter inobjectorarray. -
allow_reservedDefines whether reserved characters:/?#[]@!$&'()*+,;=are allowed in the parameter value. -
example = ...can be a method reference orjson!(...). The given example will override any examples of the underlying parameter type. -
value_type = ...can be used to override the default type used by fields in the OpenAPI specification. This is useful when the default type does not correspond to the actual type, for example, when using third-party types not defined inToSchemaorprimitivetypes. The value can be any Rust type that can be serialized to JSON under normal circumstances, or a custom type like _Object_._Object`_ that will be rendered as a generic OpenAPI object. -
inlineIf enabled, the definition of this field type must come fromToSchema, and this definition will be inlined. -
default = ...can be a method reference orjson!(...). -
format = ...can be a variant of theKnownFormatenum, or an open value in string form. By default, the format is inferred from the type of the attribute according to the OpenApi specification. -
write_onlyDefines that the attribute is only used for write operations POST,PUT,PATCH and not GET. -
read_onlyDefines that the attribute is only used for read operations GET and not POST,PUT,PATCH. -
nullableDefines whether the attribute can benull(note that this is different from not required). -
required = ...is used to force the parameter to be required. See rules. -
rename = ...can be used as an alternative toserde'srename. It actually provides the same functionality. -
multiple_of = ...is used to define a multiple of the value. The parameter value is considered valid only when the parameter value is divided by the value of this keyword and the result is an integer. The multiple value must be strictly greater than0. -
maximum = ...is used to define the upper limit of the value, including the current value. -
minimum = ...is used to define the lower limit of the value, including the current value. -
exclusive_maximum = ...is used to define the upper limit of the value, excluding the current value. -
exclusive_minimum = ...is used to define the lower limit of the value, excluding the current value. -
max_length = ...is used to define the maximum length of astringtype value. -
min_length = ...is used to define the minimum length of astringtype value. -
pattern = ...is used to define a valid regular expression that the field value must match. The regular expression adopts the ECMA-262 version. -
max_items = ...can be used to define the maximum number of items allowed in anarraytype field. The value must be a non-negative integer. -
min_items = ...can be used to define the minimum number of items allowed in anarraytype field. The value must be a non-negative integer. -
with_schema = ...uses a function reference to create aschemainstead of the defaultschema. The function must satisfy the definitionfn() -> Into<RefOr<Schema>>. It does not receive any parameters and must return any value that can be converted toRefOr<Schema>. -
additional_properties = ...is used to define free-form types formap, such asHashMapandBTreeMap. Free-form types allow any type to be used in map values. Supported formats areadditional_propertiesandadditional_properties = true.
Field nullability and required rules
Some of the rules for nullability and requiredness of ToParameters field attributes can also be used for ToSchema field attributes. See rules.
Partial #[serde(...)] attributes support
The ToParameters derive currently supports some serde attributes. These supported attributes will be reflected in the generated OpenAPI documentation. The following attributes are currently supported:
rename_all = "..."is supported at the container level.rename = "..."is only supported at the field level.defaultis supported at the container and field level according to serde attributes.skip_serializing_if = "..."is only supported at the field level.with = ...is only supported at the field level.skip_serializing = "..."is only supported at the field or variant level.skip_deserializing = "..."is only supported at the field or variant level.skip = "..."is only supported at the field level.
Other serde attributes will affect serialization, but will not be reflected in the generated OpenAPI documentation.
Examples
Demonstrates the use of the #[salvo(parameters(...))] container attribute in conjunction with ToParameters on path parameters, and inlines a query field:
Use value_type to override the String type with the i64 type.
Use value_type to override the String type with the Object type. In the OpenAPI specification, the Object type will be displayed as type:object.
You can also use a generic to override the default type of a field.
You can even use one [Vec] to override another [Vec].
We can use another ToSchema to override the field type.
Example of attribute value validation
Use schema_with to manually implement the schema for the field.
-
rename_all = ...: Supports a syntax similar toserdeto define rules for renaming fields. If both#[serde(rename_all = "...")]and#[salvo(schema(rename_all = "..."))]are defined,#[serde(rename_all = "...")]is preferred. -
symbol = ...: A string literal used to define the name path of the structure on the line in OpenAPI. For example,#[salvo(schema(symbol = "path.to.Pet"))]. -
default: You can use theDefaultimplementation of the structure to fill all fields with default values.
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 get the necessary error information, we also need to implement EndpointOutRegister for this error:
This error set defines all possible error messages that the entire web application may return. However, in many cases, our Handler may only contain a few specific error types. In this case, you can use status_codes to filter out the required error type information: