Repository navigation
Use schemars Serialization contract for OperationOutput #250
Description
I've just noticed that this crate is using the default Deserialization contract of schemars for OperationOutputs such as for axum::Json.
This is incorrect but has probably gone unnoticed until now, because the Deserialization contract of schemars generates a superset jsonschema of the one that would've been generated by the Serialization contract.
Let me highlight an example:
#[derive(Debug, Serialize, JsonSchema)]
pub struct Response {
pub rating: Option<i64>,
}which would currently result in a response schema as such:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Response",
"type": "object",
"properties": {
"rating": {
"type": [
"integer",
"null"
],
"format": "int64"
}
}
}But that is incorrect in the context of a serialization operation, where the rating field should be defined as required, as it will always exist in a response, but be either a null or i64.
The correct jsonschema would look like this:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Response",
"type": "object",
"properties": {
"rating": {
"type": [
"integer",
"null"
],
"format": "int64"
}
},
"required": [
"rating"
]
}Note the required field. At the moment aide always utilizies the Deserialization contract, even for responses.
We'll probably have to add two schema generators to the gen context and use the serialization generator for all OperationOuput implementations.
Although this change may initially seem like a breaking change, all it really does is narrow down the possible states of fields within responses.
What it used to generate: "Field A can be an i64, null, or not exist"
What it would now generate: "Field A can be an i64 or null"
So we're really just removing the case of "not exists" from the field schema.
Here's a complete snippet to reproduce it yourself:
#[cfg(test)]
mod tests {
use schemars::generate::SchemaSettings;
use schemars::{JsonSchema, SchemaGenerator};
use serde::Serialize;
#[derive(Debug, Serialize, JsonSchema)]
pub struct Response {
pub rating: Option<i64>,
}
#[test]
fn schema_for_serialize() {
let generate = SchemaGenerator::new(SchemaSettings::default().for_serialize()); // <- Uses the Serialize schemars contract
let schema = generate.into_root_schema_for::<Response>();
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
#[test]
fn schema_for_deserialize() {
let generate = SchemaGenerator::new(SchemaSettings::default().for_deserialize()); // <- Uses the Deserialize schemars contract
let schema = generate.into_root_schema_for::<Response>();
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
}
}
Activity
dnlsndr commentedon Nov 17, 2025
@jplatte tagging you here, as i'm seeing you're already preparing a 0.16 release
jplatte commentedon Nov 17, 2025
I saw your PR(s). I've just got many more important projects besides aide. I plan to review your change at some point and merge it before 0.16.0.
dnlsndr commentedon Nov 18, 2025
There might be one additional tweak that would make sense to introduce with this PR. At the moment we're using the
draft07preset that generates a non-openapi-optimized schema. Schemars has aopenapi3preset that changes a few additional things to be more in-line with the OpenAPI 3.0/3.1 spec.Check this out: https://docs.rs/schemars/latest/schemars/generate/struct.SchemaSettings.html#implementations
I believe it would make sense to update this too, as this PR could anyways already be seen as a breaking change.
jplatte commentedon Jan 16, 2026
Since this is turning out to be pretty complex based on the discussion in #254, I'm pushing this to after the 0.16 release.
feat: reconcile deserialize/serialize schemas into one components nam…
atarutin commentedon Oct 17, 2026
I opened #303 for this. It uses two generators as in #254 and answers the review question there about types used on both sides. Response refs use a private path. When the document is finished, a component whose schemas differ under the two contracts (directly, or through a component it references) is split into
{Name}-Input/{Name}-Output, the same convention as FastAPI/Pydantic'sseparate_input_output_schemas. Every$refis rewritten to match, and the result doesn't depend on route registration order. It's breaking, so it may fit the 0.16 window (#270).