GREsau / schemars

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

flatten of `Option<Enum>` generates schema where enum is required #317

Open meskill opened 2 months ago

meskill commented 2 months ago

Consider the next types:

#[derive(JsonSchema)]
enum Enum {
    Var1(String),
    Var2(u32)
}

#[derive(JsonSchema)]
struct FlattenOptionEnum {
    #[serde(flatten)]
    enum_: Option<Enum>
}

The generated schema looks like

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "FlattenOptionEnum",
  "type": "object",
  "oneOf": [
    {
      "type": "object",
      "properties": {
        "Var1": {
          "type": "string"
        }
      },
      "additionalProperties": false,
      "required": [
        "Var1"
      ]
    },
    {
      "type": "object",
      "properties": {
        "Var2": {
          "type": "integer",
          "format": "uint32",
          "minimum": 0
        }
      },
      "additionalProperties": false,
      "required": [
        "Var2"
      ]
    }
  ]
}

Hence, forcing the value from enum to be specified by the schema i.e. basically ignoring Option

Serde handles this cases properly by handling optional case. Check the playground

meskill commented 2 months ago

possible fix in https://github.com/GREsau/schemars/pull/318

GREsau commented 2 months ago

This is a valid bug, but I think fixing it may be quite complicated. It's also related to #48 in that it depends on the deserialize vs serialize behaviour we're trying to describe.

Consider these types:

#[derive(JsonSchema)]
pub struct MyStruct {
    pub my_int: i32,
    #[schemars(flatten)]
    pub enum1: Option<MyEnum1>,
    #[schemars(flatten)]
    pub enum2: Option<MyEnum1>,
}

#[derive(JsonSchema)]
pub enum MyEnum1 {
    Foo(i32),
    Bar(i32),
    Foobar(i32),
}

#[derive(JsonSchema)]
pub enum MyEnum2 {
    Baz(i32),
    Qux(i32),
}

What should the schema for MyStruct look like?

When serialized to JSON, the value will be an object that must contain the properties:

There are several ways to represent that schema. For example, using dependentSchemas to disallow the mutually-exclusive properties:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "MyStruct",
  "type": "object",
  "properties": {
    "my_int": {
      "type": "integer",
      "format": "int32"
    },
    "Foo": {
      "type": "integer",
      "format": "int32"
    },
    "Bar": {
      "type": "integer",
      "format": "int32"
    },
    "Foobar": {
      "type": "integer",
      "format": "int32"
    },
    "Baz": {
      "type": "integer",
      "format": "int32"
    },
    "Qux": {
      "type": "integer",
      "format": "int32"
    }
  },
  "dependentSchemas": {
    "Foo": {
      "properties": {
        "Bar": false,
        "Foobar": false
      }
    },
    "Bar": {
      "properties": {
        "Foo": false,
        "Foobar": false
      }
    },
    "Foobar": {
      "properties": {
        "Foo": false,
        "Bar": false
      }
    },
    "Baz": {
      "properties": {
        "Qux": false
      }
    },
    "Qux": {
      "properties": {
        "Baz": false
      }
    }
  },
  "required": [
    "my_int"
  ]
}

But if we're instead considering what JSON values will successfully deserialize to a MyStruct via serde_json, then all of the following are valid:

So if we're strictly following the deserialization behaviour, then the schema would be:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "MyStruct",
  "type": "object",
  "properties": {
    "my_int": {
      "type": "integer",
      "format": "int32"
    }
  },
  "required": [
    "my_int"
  ]
}

...which seems much less useful

GREsau commented 2 months ago

This is a special case of https://github.com/GREsau/schemars/issues/48, but I'll leave this issue open because it's a complicated case in itself, and I don't want to further overcomplicate the other issue