GREsau / schemars

Generate JSON Schema documents from Rust code
https://graham.cool/schemars/
MIT License
791 stars 220 forks source link

Nested internally tagged enums #311

Open bradzacher opened 1 month ago

bradzacher commented 1 month ago

I have the following enums:

#[derive(JsonSchema, Serialize)]
#[serde(tag = "foo")]
enum Foo {
  Bar(Bar),
}
#[derive(JsonSchema, Serialize)]
#[serde(tag = "bar")]
enum Bar {
  Baz(Baz),
}

#[derive(JsonSchema, Serialize)]
#[schemars(deny_unknown_fields)]
struct Baz {
  prop: String
}

And it generates the following JSON schema

{
  "oneOf": [
    {
      "type": "object",
      "oneOf": [
        {
          "type": "object",
          "required": [
            "bar",
            "prop"
          ],
          "properties": {
            "bar": {
              "type": "string",
              "enum": [
                "Baz"
              ]
            },
            "prop": {
              "type": "string"
            }
          },
          "additionalProperties": false
        }
      ],
      "required": [
        "foo"
      ],
      "properties": {
        "foo": {
          "type": "string",
          "enum": [
            "Bar"
          ]
        }
      }
    }
  ]
}

For example attempting to validate this object:

{
  "foo": "Bar",
  "bar": "Baz",
  "prop": ""
}

Leads to an error like this in tools like jsonschemavalidator

Property 'foo' has not been defined and the schema does not allow additional properties.

or this in vscode:

Property foo is not allowed.

If you turn off #[schemars(deny_unknown_fields)] for the inner struct, Baz, then the schema passes - however this also means that ofc you can add extra properties -- which is sub-optimal.

If you also add #[schemars(deny_unknown_fields)] to the enums then you end up with even worse errors because the bar and prop fields aren't declared on the outer object - so they also error.

The issue is that from what I can tell validators do not "correctly" consider additionalProperties within nested oneOfs.

I don't know the best path forward here. I can see that if we were to flatten the nested definitions down into a single-level of oneOf then it will work as expected -- but I suspect this would be quite a complicated thing to do.