Skip to content

vld-utoipa + axum: Query is missing OpenAPI validation fields #3

@jakubzet

Description

@jakubzet

Hello! I've recently discovered vld - it is like a missing piece in Axum stack, congratulations and thank you very much for your work!

I'm writing because of problem I found with Query parameters appearance in OpenAPI schema. I'll show a working example of Json for comparison:

vld::schema! {
    #[derive(Debug, Serialize, Deserialize)]
    struct BodyPayload {
        sample: String => vld::string().min(1),
    }
}
impl_to_schema!(BodyPayload);

// Later used in the Axum handler like this:
// async fn some_post_endpoint(
//    VldJson(body): VldJson<BodyPayload>,

The above works perfectly fine - I have my payload validated in the endpoint. In OpenAPI schema it appears like this:

"properties": { "sample": {"type": "string", "minLength": 1 }}

However - with the Query it works only partially. With either of structs from code fragment below, I only have endpoint validation, but miss the OpenAPI definition.

// Neither this...
vld::schema! {
    #[derive(Debug, Serialize, Deserialize, IntoParams)]
    struct QueryParams {
        sample: String => vld::string().min(16),
    }
}
impl_to_schema!(QueryParams);

// ...or this works :(
#[derive(Debug, Deserialize, Validate, IntoParams)]
pub struct QueryParams {
    #[vld(vld::string().min(16))]
    sample: String,
}

// Later used in the Axum handler like this:
// async fn get_todos(
//    Query(query): Query<QueryParams>,

The OpenAPI schema looks like this - it totally ignored the min length of string:

"parameters": {
    "0":
        "name": "sample",
        "in": "query"
        "required": true
        "schema": {	
            "type": "string"
        }
}

In order to actually have some validation rules printed in schema it is necessary to add param macro from utoipa's IntoParams explicitly, like this:

vld::schema! {
    #[derive(Debug, Serialize, Deserialize, IntoParams)]
    struct QueryParams {
        #[param(min_length = 16)]
        sample: String => vld::string().min(16),
    }
}
impl_to_schema!(QueryParams);

As you can see though, this is error-prone as validation rules have to be placed in two places, instead of being derived like it's wonderfully done in Json's case.

Do you see I'm doing something wrong, or perhaps it's a bug we should try to tackle? I'm open for any help needed with this one, I'm just not sure where to start digging with this :) Thanks in advance for your time!

EDIT: Just noticed it also happens for Path, not only Query. So perhaps into all structs implementing IntoParams trait?

EDIT 2: After a few tries, I've actually managed to overcome the issue, this is a code example I modified from utoapi's axum example, with my notes marked as NOTE: comments:

use std::io::Error;
use std::net::{Ipv4Addr, SocketAddr};
use tokio::net::TcpListener;
use utoipa::{IntoParams, OpenApi};
use utoipa_axum::{router::OpenApiRouter, routes};
use utoipa_swagger_ui::SwaggerUi;
use vld;
use vld_axum::VldJson as Json;
use vld_axum::VldQuery as Query;
use vld_utoipa::impl_to_schema;

// NOTE: Step 1. I declare structs for Query params and Json (body):
vld::schema! {
    #[derive(Debug, IntoParams)]
    struct SearchParams {
        sample: String => vld::string().min(1).max(200),
    }
}
impl_to_schema!(SearchParams);

vld::schema! {
    #[derive(Debug)]
    struct BodyPayload {
        sample: String => vld::string().min(1),
    }
}
impl_to_schema!(BodyPayload);

#[derive(OpenApi)]
// NOTE: Step 2. I need to provide the struct manually into utoipa macro, it fills the "components" in OpenAPI schema
#[openapi(components(schemas(SearchParams)))]
struct ApiDoc;

fn router() -> OpenApiRouter {
    OpenApiRouter::new().routes(routes!(search_todos))
}

#[utoipa::path(
    get,
    path = "/search",
    // NOTE: Step 3. I need to explicitly add params here, as a Query one: this adds query parameter to endpoint's parameters
    params(("SearchParams" = SearchParams, Query)),
    responses(
        (status = 200, description = "List matching todos by query")
    )
)]
async fn search_todos(Query(query): Query<SearchParams>, Json(body): Json<BodyPayload>) -> String {
    println!("Query {:?}", query);
    println!("Body {:?}", body);
    "Response".to_string()
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    let (router, api) = OpenApiRouter::with_openapi(ApiDoc::openapi())
        .nest("/api/v1/todos", router())
        .split_for_parts();

    let router =
        router.merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", api.clone()));

    let address = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 8080));
    let listener = TcpListener::bind(&address).await?;
    axum::serve(listener, router.into_make_service()).await
}

As we can observe, in steps 2 and 3 in need to explicitly add SearchParams into utoipa, while BodyPayload struct does not require this. Would that be an issue which could be solved on vld side, or it's rather utoipa's fault here?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions