Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generics: the trait bound Value: ToSchema is not satisfied #1128

Closed
wuzi opened this issue Oct 14, 2024 · 5 comments · Fixed by #1132
Closed

Generics: the trait bound Value: ToSchema is not satisfied #1128

wuzi opened this issue Oct 14, 2024 · 5 comments · Fixed by #1132

Comments

@wuzi
Copy link

wuzi commented Oct 14, 2024

Hey!

When using a generic as the body property in path macro I'm getting two errors:

  • error[E0277]: the trait bound Value: ToSchema is not satisfied
  • error[E0277]: the trait bound Value: ComposeSchema is not satisfied

Code (the relevant parts):

    use std::net::{Ipv4Addr, SocketAddr};
    
    use std::io::Error;
    use tokio::net::TcpListener;
    use utoipa::{openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, Modify, OpenApi};
    use utoipa_axum::router::OpenApiRouter;
    use utoipa_redoc::{Redoc, Servable};

    #[derive(Debug, Clone, Default, Serialize, ToSchema, Deserialize)]
    pub struct ResponseSchema<D: utoipa::ToSchema, M = Option<serde_json::Value>> {
        pub data_one: D,
        #[schema(value_type=Object)]
        pub data_two: M,
    }

    #[derive(Serialize, Deserialize, ToSchema, Clone)]
    struct Todo {
        id: i32,
        #[schema(example = "Buy groceries")]
        value: String,
        done: bool,
    }

    #[utoipa::path(
        get,
        path = "",
        tag = TODO_TAG,
        responses(
            (status = 200, description = "List all todos successfully", body = [ResponseSchema<Todo>])
        )
    )]
    async fn list_todos(State(store): State<Arc<Store>>) -> Json<Vec<Todo>> {
        let todos = store.lock().await.clone();

        Json(todos)
    }
[dependencies]
axum = { version = "0.7.5", features = ["original-uri"] }
axum-extra = "0.9.3"
axum-macros = "0.4.1"
utoipa = { version = "5.0.0", features = ["axum_extras"] }
utoipa-axum = { version = "0.1.0" }
utoipa-redoc = { version = "5.0.0", features = ["axum"] }
tokio = { version = "1.40.0", features = ["full"] }
hyper = "1.4.1"
serde_json = "1.0.113"
serde = { version = "1.0.210", features = ["derive"] }

What is weird is that i swear it was working a few hours ago and it started after I updated the crate to 5.0.0. But I don't have my previous lock file to compare what changed.

I've ran cargo clean and tried in a new project as well.

Full code ```
use std::net::{Ipv4Addr, SocketAddr};

use std::io::Error;
use tokio::net::TcpListener;
use utoipa::{openapi::security::{ApiKey, ApiKeyValue, SecurityScheme}, Modify, OpenApi};
use utoipa_axum::router::OpenApiRouter;
use utoipa_redoc::{Redoc, Servable};

const TODO_TAG: &str = "todo";

#[tokio::main]
async fn main() -> Result<(), Error> {
    #[derive(OpenApi)]
    #[openapi(
        modifiers(&SecurityAddon),
        tags(
            (name = TODO_TAG, description = "Todo items management API")
        )
    )]
    struct ApiDoc;

    struct SecurityAddon;

    impl Modify for SecurityAddon {
        fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
            if let Some(components) = openapi.components.as_mut() {
                components.add_security_scheme(
                    "api_key",
                    SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("todo_apikey"))),
                )
            }
        }
    }

    let (router, api) = OpenApiRouter::with_openapi(ApiDoc::openapi())
        .nest("/api/v1/todos", todo::router())
        .split_for_parts();

    let router = router
        .merge(Redoc::with_url("/redoc", api.clone()));

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

mod todo {
    use std::sync::Arc;

    use axum::{
        extract::{State},
        Json,
    };
    use serde::{Deserialize, Serialize};
    use tokio::sync::Mutex;
    use utoipa::{ToSchema};
    use utoipa_axum::{router::OpenApiRouter, routes};

    #[derive(Debug, Clone, Default, Serialize, ToSchema, Deserialize)]
    pub struct ResponseSchema<D, M = Option<serde_json::Value>> {
        pub data_one: D,
        #[schema(value_type=Object)]
        pub data_two: M,
    }

    use crate::TODO_TAG;

    /// In-memory todo store
    type Store = Mutex<Vec<Todo>>;

    /// Item to do.
    #[derive(Serialize, Deserialize, ToSchema, Clone)]
    struct Todo {
        id: i32,
        #[schema(example = "Buy groceries")]
        value: String,
        done: bool,
    }

    pub(super) fn router() -> OpenApiRouter {
        let store = Arc::new(Store::default());
        OpenApiRouter::new()
            .routes(routes!(list_todos))
            .with_state(store)
    }

    /// List all Todo items
    ///
    /// List all Todo items from in-memory storage.
    #[utoipa::path(
        get,
        path = "",
        tag = TODO_TAG,
        responses(
            (status = 200, description = "List all todos successfully", body = [ResponseSchema<Todo>])
        )
    )]
    async fn list_todos(State(store): State<Arc<Store>>) -> Json<Vec<Todo>> {
        let todos = store.lock().await.clone();

        Json(todos)
    }
}
```
@juhaku
Copy link
Owner

juhaku commented Oct 14, 2024

@wuzi Yeah, this is actually due to the full generic support. Since serde_json::Value actually does not implement ToSchema so the error occurs you are facing. The full generics support requires that all generic arguments implement ToSchema trait. So only way to go around this is to wrap the serde_json::Value to a new type and implement ToSchema for it.

pub struct ResponseSchema<D: utoipa::ToSchema, M = Option<serde_json::Value>> { // <-- here is the issue
  ...
}

This should work.

#[derive(ToSchema)]
struct MyJson(serde_json::Value);
pub struct ResponseSchema<D: utoipa::ToSchema, M = Option<MyJson>> {
  ...
}

Another way would be that serde would implement ToSchema trait but I am not convinced that they would add it.

@wuzi
Copy link
Author

wuzi commented Oct 14, 2024

Thank you for the quick reply @juhaku !

This solves the issue, I thought it was supported and I was forgetting to add some trait because I think it worked before.

But I can work with that.

Thank you.

@wuzi wuzi closed this as completed Oct 14, 2024
@juhaku
Copy link
Owner

juhaku commented Oct 14, 2024

Anytime, yeah it has somewhat worked before because utoipa 4.x.x did not really support generics thus there was no restrictions on generic type bounds. I forgot to mention this in migration guide. And I added the note there just a moment ago.

Perhaps I can try to see if I can implement ToSchema for the serde_json::Value type so there is no need wrap it, but anyway this kind of wrapping is necessary for other types that are not implementing ToSchema and if ToSchema cannot be implemented directly to the type.

juhaku added a commit that referenced this issue Oct 15, 2024
This commit adds direct `ToSchema` implementation for `serde_json::Value`.

Fixes #1128
juhaku added a commit that referenced this issue Oct 15, 2024
This commit adds direct `ToSchema` implementation for `serde_json::Value`.

Fixes #1128
juhaku added a commit that referenced this issue Oct 15, 2024
This commit adds direct `ToSchema` implementation for `serde_json::Value`.

Fixes #1128
@johnallen3d
Copy link

Does this actually work @juhaku?

#[derive(ToSchema)]
struct MyJson(serde_json::Value);

I tried something similar with another crate (num-decimal).

#[derive(ToSchema)]
pub struct Decimal(pub num_decimal::Num);

However this errors with:

error[E0277]: the trait bound `num_decimal::Num: PartialSchema` is not satisfied
   --> calculate/src/decimal.rs:26:24
    |
26  | pub struct Decimal(pub num_decimal::Num);
    |                        ^^^^^^^^^^^^^^^^ the trait `ComposeSchema` is not implemented for `num_decimal::Num`, which is required by `num_decimal::Num: PartialSchema`
    |
    = help: the following other types implement trait `PartialSchema`:
              ()
              serde_json::Value
    = note: required for `num_decimal::Num` to implement `PartialSchema`

but I can't implement PartialSchema for num_decimal::Num because neither are local.

@juhaku
Copy link
Owner

juhaku commented Oct 22, 2024

To be honest after a second thought, it should not work directly. Because new type will actually just unwrap to the inner type. And unless the type implements ToSchema it will give the error. For what happens in case of serde_json::Value is that utoipa has some internal logic that checks whether type is Value and treats it as JSON if I remember right. However since utoipa 5.1.0 it has direct ToSchema implementation for serde_value::Json #1132.

Only way forward in this case is by some means to create a schema for the type. More details here #507 and #790 (comment)

Or you could alias Num as f64 or to any other type that implements ToSchema if that would make sense https://github.com/juhaku/utoipa/tree/master/utoipa-config

Or you could try something similar to what is done here in generic schema example:

fn get_coord_schema<T: PartialSchema>() -> Object {
ObjectBuilder::new()
.property("x", T::schema())
.required("x")
.property("y", T::schema())
.required("y")
.description(Some("this is the coord description"))
.build()
}
#[derive(Serialize, ToSchema)]
pub struct MyObject<T: geo_types::CoordNum + PartialSchema> {
#[schema(schema_with=get_coord_schema::<T>)]
at: geo_types::Coord<T>,
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants