rustactix-web

How do I generate the openapi schema for an optional query parameter in utoipa?


New to rust and friends. Using actix-web, utoipa, and utoipa-swagger-ui.

I'm trying to write a basic end point with an optional query parameter. For a minimal example, let's say it's a GET to list objects, with an optional limit parameter. The details don't matter as it's actually the OpenAPI stuff I'm struggling with

#[derive(Deserialize, ToSchema)]
struct QueryParams {
    limit: Option<bool>
}

#[utoipa::path(get, path="/users", params(("limit", Query))]
#[actix-web::get("/users")]
pub async fn get_users(query_params: web::Query<QueryParams>) -> impl Responder {
  HttResponse::Ok().body("Coming soon")
}

I have this endpoint working as is, and if I open the swagger-ui page I see the end-point and the query parameters.

But no-matter what I do, I can't get utoipa to mark the parameter optional in the generated schema: it is always marked as required.

This seems a fundamental ability, but I can't find any documentation explaining it.

This may well be a duplicate and, if so, I'm happy to close and delete this question, but I've been fighting with this for a couple of days

---- Update

Apparently, my example above was too minimal to show the problem. Here is a complete program, which I've cut down from my actual application.


use actix_web::{get, middleware, post, web, App, HttpResponse, HttpServer, Responder};

use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use serde::Deserialize;
use utoipa::ToSchema;


#[derive(Deserialize, ToSchema)]
struct QFilename {
    filename: Option<String>,
}

#[utoipa::path(get, path = "/v2/users", params(("filename", Query,  description = "download filename")))]
#[get("/users")]
async fn get_users(_query_params: web::Query<QFilename>) -> impl Responder {
    HttpResponse::Ok().body("Hello world!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    #[derive(OpenApi)]
    #[openapi(paths(
        get_users,
    ))]
    struct OpenApiDoc;

    let openapi = OpenApiDoc::openapi();
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    HttpServer::new(move || {
        App::new()
            .wrap(middleware::Logger::default())
            .service(
                web::scope("/v2")
                    .service(get_users),
            )
            .service(
                SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-docs/openapi.json", openapi.clone()),
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

This is my Cargo.toml

[package]
name = "minimal"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
utoipa = { version = "4", features = ["actix_extras"] }
actix-web = "4"
tokio = { version = "1", features = ["full"] }
utoipa-swagger-ui = { version = "6", features = ['actix-web'] }
env_logger = "0.11.2"
log = "0.4.20"
serde = { version = "1", features = ["derive"] }

This is the result

Swagger UI screen

And this is the generated openapi spec

{
   "openapi":"3.0.3",
   "info":{
      "title":"minimal",
      "description":"",
      "license":{
         "name":""
      },
      "version":"0.1.0"
   },
   "paths":{
      "/v2/users":{
         "get":{
            "tags":[
               "crate"
            ],
            "operationId":"get_users",
            "parameters":[
               {
                  "name":"filename",
                  "in":"query",
                  "description":"download filename",
                  "required":true
               }
            ],
            "responses":{
               
            }
         }
      }
   }
}

Solution

  • You didn't specify a type for the parameter.

    It should be:

    #[utoipa::path(get, path = "/v2/users", params(("filename" = QFilename, Query, description = "download filename")))]
    #[get("/users")]
    pub async fn get_users(_query_params: web::Query<QFilename>) -> impl Responder {
        // ...
    }
    
    // ...
    
    #[derive(OpenApi)]
    #[openapi(paths(get_users), components(schemas(QFilename)))]
    struct OpenApiDoc;
    

    And then the JSON generated:

    {
       "openapi":"3.0.3",
       "info":{
          "title":"my_test",
          "description":"",
          "license":{
             "name":""
          },
          "version":"0.1.0"
       },
       "paths":{
          "/v2/users":{
             "get":{
                "tags":[
                   "crate"
                ],
                "operationId":"get_users",
                "parameters":[
                   {
                      "name":"filename",
                      "in":"query",
                      "description":"download filename",
                      "required":true,
                      "schema":{
                         "$ref":"#/components/schemas/QFilename"
                      }
                   }
                ],
                "responses":{
                   
                }
             }
          }
       },
       "components":{
          "schemas":{
             "QFilename":{
                "type":"object",
                "properties":{
                   "filename":{
                      "type":"string",
                      "nullable":true
                   }
                }
             }
          }
       }
    }
    

    Screenshot:

    screenshot

    This still shows "* required", because the field filename is optional, but the object QFilename is required (you never said otherwise). In JSON, (like Swagger's "Try it out" interface), that means we can send an empty object {}, but not nothing. In query language, there is no difference, which is why you expected it to not say "required".

    If you want the parameter to show up as non-required, you can wrap it in Option. Not the real parameter - then you'll have double Option, but the type you describe to utoipa:

    #[utoipa::path(get, path = "/v2/users", params(("filename" = Option<QFilename>, Query, description = "download filename")))]
    #[get("/users")]
    pub async fn get_users(_query_params: web::Query<QFilename>) -> impl Responder {
        // ...
    }