aide icon indicating copy to clipboard operation
aide copied to clipboard

Add additional schemas that are not part of handlers

Open tomgroenwoldt opened this issue 1 year ago • 7 comments

Hi :)

Is there an easy way to add additional schemas to the specification without the need to have them inside route handlers?

tomgroenwoldt avatar Nov 24 '24 14:11 tomgroenwoldt

And a similar question (probably, should have its own issue): is there an easy way to remove some schemas? For example, some new-type wrappers or nested structs.

asqarslanov avatar Feb 16 '25 01:02 asqarslanov

@tomgroenwoldt

I do this in my app like so:

   if let Some(components) = api.components.as_mut() {
       components.schemas.insert(
            "CreateAction".to_string(),
            SchemaObject {
                json_schema: schemars::schema::Schema::Object(schema_for!(CreateAction).schema),
                external_docs: None,
                example: None,
            },
        );
}

@asqarslanov I also do this in the same function I snippeted above, like so:

   if let Some(components) = api.components.as_mut() {
        components
            .schemas
            .retain(|name, schema| schema_filter(name, schema));
}

where schema_filter is a function:

fn schema_filter(schema_name: &str, _schema: &SchemaObject) -> bool {
    matches!(
        schema_name,
        "User" | "Team" |"Action"
    )
}

PeterGrace avatar Feb 21 '25 12:02 PeterGrace

@PeterGrace

Thank you! This looks like something to work with. However, I have some problems with this approach.

When I try to look inside api.components.as_mut(), right after the creation of api or inside the .finish_api_with method, it has a None value. If I manually insert my own index map with schemas (as a raw field), this has no effect. If I wrap api inside an Arc<Mutex<_>>, and manually insert schemas every time before sending the API docs to a client, the behavior just looks buggy.

The documentation of the Components struct mentions that

All objects defined within the components object will have no effect on the API unless they are explicitly referenced from properties outside the components object.

I’m not sure how to reference these properties correctly.

asqarslanov avatar Feb 21 '25 23:02 asqarslanov

@asqarslanov

I understand your issue and I experienced it as well -- the thing is that the api is initialized as Default first, and it needs to be populated by finish_api_with() before you can adjust it.

this is how I do it in my app -- I have these api.component.insert() calls inside a function called filter_api_schemas, and this is how I call it in main.rs. See how I call let mut app = app.finish_api_with() and I then call filter_api_schemas afterwards?

  let mut api = OpenApi::default();

    let app = ApiRouter::new()
        .merge(public_routes)
        .merge(protected_routes)
        .with_state(state);

    let mut app = app.finish_api_with(&mut api, api_docs).layer(
        ServiceBuilder::new()
            .layer(cors)
            .layer(session_layer)
            .layer(middleware::from_fn(session_middleware)),
    );

    api = filter_api_schemas(api);
    app = app.layer(Extension(api));

    let listener = TcpListener::bind("0.0.0.0:3003").await.unwrap();

    info!("listening on {}", listener.local_addr().unwrap());
    let _ = axum::serve(listener, app.into_make_service()).await;
pub fn filter_api_schemas(mut api: OpenApi) -> OpenApi {
    if let Some(components) = api.components.as_mut() {
        info!(
            "Available schemas before filtering: {:?}",
            components.schemas.keys().collect::<Vec<_>>()
        );
        components
            .schemas
            .retain(|name, schema| schema_filter(name, schema));

        // here we add our schemas manually
        components.schemas.insert(
            "CreateTeam".to_string(),
            SchemaObject {
                json_schema: schemars::schema::Schema::Object(schema_for!(CreateTeam).schema),
                external_docs: None,
                example: None,
            },
        );
        info!(
            "Available schemas after filtering: {:?}",
            components.schemas.keys().collect::<Vec<_>>()
        );
    }
    api
}

hope this helps!

PeterGrace avatar Feb 22 '25 00:02 PeterGrace

@PeterGrace

Now everything works as expected, thank you very much! 🔥

I suppose, the issue may be closed. Though, would be really nice if the library had a more high-level way to do this.

asqarslanov avatar Feb 22 '25 00:02 asqarslanov

Actually, there is another issue. It’s more related to Schemars rather than Aide.

Removing the schemas of datatypes, that are used in other schemas, results in “dangling” references that are shown as errors in OpenAPI front ends. It’s possible to solve this problem by inlining “inner” schemas (making them unreferenceable). For now, however, the only convenient way to do this I see is to enable the inline_subschemas option, which completely removes all schemas from the generated docs, only showing them inside endpoints. To manually choose types whose JSON Schema should be inlined, one needs to manually JsonSchema trait, which is pretty unergonomic.

Solving either of the following issues will also solve this problem:

  • https://github.com/GREsau/schemars/issues/211
  • https://github.com/GREsau/schemars/issues/353

asqarslanov avatar Feb 22 '25 14:02 asqarslanov

right after the creation of api or inside the .finish_api_with method, it has a None value

The callback is called before the api schemas are populated here, I don't think it was on purpose so it should be a bug.

tamasfe avatar Feb 22 '25 23:02 tamasfe