Add additional schemas that are not part of handlers
Hi :)
Is there an easy way to add additional schemas to the specification without the need to have them inside route handlers?
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.
@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
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
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
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.
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
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.