schemars icon indicating copy to clipboard operation
schemars copied to clipboard

Schemars overwrites declarations if there are structs with the same name

Open NeoLegends opened this issue 4 years ago • 5 comments

mod a {
    use super::*;

    #[derive(JsonSchema)]
    pub struct Config {
        test: String,
    }
}
mod b {
    use super::*;

    #[derive(JsonSchema)]
    pub struct Config {
        test2: String,
    }
}

#[derive(JsonSchema)]
pub struct Config2 {
    a_cfg: a::Config,
    b_cfg: b::Config,
}

generates the following, invalid schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Config2",
  "type": "object",
  "required": [
    "a_cfg",
    "b_cfg"
  ],
  "properties": {
    "a_cfg": {
      "$ref": "#/definitions/Config"
    },
    "b_cfg": {
      "$ref": "#/definitions/Config"
    }
  },
  "definitions": {
    "Config": {
      "type": "object",
      "required": [
        "test"
      ],
      "properties": {
        "test": {
          "type": "string"
        }
      }
    }
  }
}

This is because there are two member structs with the same name, although they sit at different paths. This use case occurs in our systems because some (more top-level modules) export their own Config structs which we then aggregate into one, global config for which we want to generate a schema for.

Is this expected behavior? What can we do to fix this? Would it make sense to extend schemars with support for this use case? It could automatically incorporate, e. g. the parent module name / the module path into the name of the definitions, such that these clashes can be avoided.

NeoLegends avatar Nov 19 '20 11:11 NeoLegends

Note that turning on inlining is a possible workaround. This may make the schema more verbose though.

https://docs.rs/schemars/0.8.0/schemars/gen/struct.SchemaSettings.html#structfield.inline_subschemas

NeoLegends avatar Dec 02 '20 11:12 NeoLegends

I have found and successfully used a workaround: Using #[schemars(rename = "......")] on one of the structs with duplicated definitions.

pashadia avatar Feb 03 '21 08:02 pashadia

This is a tricky one - schemars somewhat naively treats the schema_name (set to the type name by schemars_derive) as a unique identifier for a type/schema.

I've considered trying to find a better way to do it that properly differentiates types with the same name. Perhaps we could use TypeId, although that gets tricky with borrowed types, since I think for our purposes we would want T and &T to be considered the same type.

GREsau avatar Mar 22 '21 11:03 GREsau

Currently, the suggestions from @NeoLegends and @pashadia are the best way to work around this. However, rename = '...' relies on one of the structs being in your crate, which may not always be the case.

I can think of two potential ways we could fix this, both rely on adding a new attribute:

Option 1: rename_type

#[derive(JsonSchema)]
pub struct Config2 {
    a_cfg: a::Config,
    #[schemars(rename_type = "ConfigB")]
    b_cfg: b::Config,
}

Which would produce:

{
  "properties": {
    "a_cfg": { "$ref": "#/definitions/Config" },
    "b_cfg": { "$ref": "#/definitions/ConfigB" }
  },
  "definitions": {
    "Config": { /* ... */ },
    "ConfigB": { /* ... */ }
  },
  /* ... */
}

Option 2: inline

#[derive(JsonSchema)]
pub struct Config2 {
    a_cfg: a::Config,
    #[schemars(inline)]
    b_cfg: b::Config,
}

Which would produce:

{
  "properties": {
    "a_cfg": {
      "$ref": "#/definitions/Config"
    },
    "b_cfg": {
        "type": "object",
        "required": [
          "test2"
        ],
        "properties": {
          "test2": {
            "type": "string"
          }
        }
      }
  },
  "definitions": {
    "Config": { /* ... */ }
  },
  /* ... */
}

GREsau avatar Mar 22 '21 11:03 GREsau

A possible alternative fix would be to just treat schema_name() as a "friendly" name (used as the top-level schema title and key within definitions, as it is today), and have a separate function to return a unique name/ID which doesn't appear in the resulting schema. Then schemars can detect when two types with the same friendly name are added in the same schema, and fix it by altering one of their friendly names, e.g. just append a "2" to the end

A simple implementation of the unique ID would be to concatenate the the type's module path with the friendly name, although we'd need to be careful re generic types, e.g. ensure that HashMap<a::Config> and HashMap<b::Config> have different IDs.

GREsau avatar Apr 25 '21 20:04 GREsau

This is finally fixed in v0.8.14 😄

GREsau avatar Sep 17 '23 19:09 GREsau