utoipa icon indicating copy to clipboard operation
utoipa copied to clipboard

Schema for internally tagged structs does not adhere to OpenAPI spec

Open 1lutz opened this issue 2 years ago • 3 comments

I would like to use the following enum (note that NumberParam has a "tag" parameter):

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum NumberParam {
    Static { value: usize },
    Derived(DerivedNumber),
}

#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DerivedNumber {
    pub attribute: String,
    pub factor: f64,
    pub default_value: f64,
}

At the moment Utoipa will generate this schema:

{
  "openapi": "3.0.3",
  "info": {
    "title": "Dummy API",
    "version": "0.7.0"
  },
  "paths": {
  },
  "components": {
    "schemas": {
      "NumberParam": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "value",
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "static"
                ]
              },
              "value": {
                "type": "integer",
                "minimum": 0.0
              }
            }
          },
          {
            "allOf": [
              {
                "$ref": "#/components/schemas/DerivedNumber"
              },
              {
                "type": "object",
                "required": [
                  "type"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "derived"
                    ]
                  }
                }
              }
            ]
          }
        ],
        "discriminator": {
          "propertyName": "type"
        }
      },
      "DerivedNumber": {
        "type": "object",
        "required": [
          "attribute",
          "factor",
          "defaultValue"
        ],
        "properties": {
          "attribute": {
            "type": "string"
          },
          "defaultValue": {
            "type": "number",
            "format": "double"
          },
          "factor": {
            "type": "number",
            "format": "double"
          }
        }
      }
    }
  }
}

As you can see, utoipa inlines the schemas of the oneOf variants. This is against the spec when there is a discriminator specified. The oneOf should only contain refs. This can also be seen when trying to generate a python client using the official program openapi-generator. It throws many errors and warnings related to this:

$ java -jar openapi-generator-cli.jar generate -i openapi_test.json -g python -o backend

[main] WARN  o.o.codegen.DefaultCodegen - Invalid inline schema defined in oneOf/anyOf in 'NumberParam'. Per the OpenApi spec, for this case when a composed s
chema defines a discriminator, the oneOf/anyOf schemas must use $ref. Change this inline definition to a $ref definition
[main] WARN  o.o.codegen.utils.ModelUtils - Failed to get the schema name: null
[main] ERROR o.o.codegen.DefaultCodegen - String to be sanitized is null. Default to ERROR_UNKNOWN
[main] ERROR o.o.codegen.DefaultCodegen - String to be sanitized is null. Default to ERROR_UNKNOWN
[main] ERROR o.o.codegen.DefaultCodegen - Failed to lookup the schema 'null' when processing oneOf/anyOf. Please check to ensure it's defined properly.       
[main] ERROR o.o.codegen.DefaultCodegen - Failed to lookup the schema 'null' when processing oneOf/anyOf. Please check to ensure it's defined properly.       
[main] WARN  o.o.codegen.DefaultCodegen - Invalid inline schema defined in oneOf/anyOf in 'NumberParam'. Per the OpenApi spec, for this case when a composed s
chema defines a discriminator, the oneOf/anyOf schemas must use $ref. Change this inline definition to a $ref definition
[main] ERROR o.o.codegen.DefaultCodegen - Failed to lookup the schema 'null' when processing oneOf/anyOf. Please check to ensure it's defined properly.       
[main] ERROR o.o.codegen.DefaultCodegen - Failed to lookup the schema 'null' when processing oneOf/anyOf. Please check to ensure it's defined properly.

To solve this problem, utoipa would need to generate named schemas for each variant and also specify a custom mapping. That can look like this:

{
  "openapi": "3.0.3",
  "info": {
    "title": "Dummy API",
    "version": "0.7.0"
  },
  "paths": {
  },
  "components": {
    "schemas": {
      "NumberParam": {
        "oneOf": [
          {
            "$ref": "#/components/schemas/NumberParamStaticWithType"
          },
          {
            "$ref": "#/components/schemas/DerivedNumberWithType"
          }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "static": "#/components/schemas/NumberParamStaticWithType",
            "derived": "#/components/schemas/DerivedNumberWithType"
          }
        }
      },
      "DerivedNumber": {
        "type": "object",
        "required": [
          "attribute",
          "factor",
          "defaultValue"
        ],
        "properties": {
          "attribute": {
            "type": "string"
          },
          "defaultValue": {
            "type": "number",
            "format": "double"
          },
          "factor": {
            "type": "number",
            "format": "double"
          }
        }
      },
      "DerivedNumberWithType": {
        "allOf": [
          {
            "$ref": "#/components/schemas/DerivedNumber"
          },
          {
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "derived"
                ]
              }
            }
          }
        ]
      },
      "NumberParamStaticWithType": {
        "type": "object",
        "required": [
          "value",
          "type"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "static"
            ]
          },
          "value": {
            "type": "integer",
            "minimum": 0.0
          }
        }
      }
    }
  }
}

What are your thoughts on this? Can this feature be added to utoipa?

1lutz avatar May 12 '23 11:05 1lutz

The problem here is that inline types are used within the complex enum. The so called fix for this would be to disallow inlined schemas when tag is provided. That is because we cannot create a reference to an inline type owning to the fact that inline schemas cannot be registered as schemas(...) since they cannot be derived from ToSchema macro.

#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase", tag = "type")]
pub enum NumberParam {
    Static { value: usize }, // <-- here
    Derived(DerivedNumber),
}

Probably this kind of limitation would be beneficial in future addition to the utoipa as it would guide users towards correct OpenAPI doc.

juhaku avatar May 12 '23 14:05 juhaku

Isn't it possible that utoipa transforms the inlined schema to a seperate type (which also has a type property)? I think at least an implementation of Modify would be able to do it.

1lutz avatar Jul 04 '23 15:07 1lutz

I had to check the spec on this. https://swagger.io/specification/#discriminator-object says

When using the discriminator, inline schemas will not be considered.

So yes it would be good if utoipa complied with this.

That said, at work, we are using the oneOf generated by utoipa, and havent hit any tooling which complains about this.

It might be easier to get openapi-generator to handle this. openapi-generator only marks it as a warning. I've seen a number of issues over at openapi-generator where they seem happy to accept inline schemas, and they dont want to be strict, c.f. https://github.com/OpenAPITools/openapi-generator/issues/1086

More at https://github.com/OpenAPITools/openapi-generator/labels/Inline%20Schema%20Handling

I bet it is [main] WARN o.o.codegen.utils.ModelUtils - Failed to get the schema name: null which is the root cause of the failures that follow.

jayvdb avatar Sep 14 '23 21:09 jayvdb