Skip to content

Use schemars Serialization contract for OperationOutput #250

Description

@dnlsndr

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

  1. dnlsndr commented on Nov 17, 2025

    @dnlsndr
    ContributorAuthor

    @jplatte tagging you here, as i'm seeing you're already preparing a 0.16 release

  2. jplatte commented on Nov 17, 2025

    @jplatte
    Collaborator

    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.

  3. dnlsndr commented on Nov 18, 2025

    @dnlsndr
    ContributorAuthor

    There might be one additional tweak that would make sense to introduce with this PR. At the moment we're using the draft07 preset that generates a non-openapi-optimized schema. Schemars has a openapi3 preset 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.

  4. added this to the 0.16 milestone on Dec 27, 2025
  5. jplatte commented on Jan 16, 2026

    @jplatte
    Collaborator

    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.

  6. removed this from the 0.16 milestone on Jan 16, 2026
  7. atarutin commented on Oct 17, 2026

    @atarutin

    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's separate_input_output_schemas. Every $ref is 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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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