OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
21.49k stars 6.51k forks source link

[BUG][PYTHON] Issue where property with "oneOf: null or list<object>" fails due to 'Multiple matching types' when either null or list found #17951

Open kdipippo opened 7 months ago

kdipippo commented 7 months ago

Bug Report Checklist

Description

🟢 I found out a workaround while working on this, noted at the bottom. But figured I'd still file the steps here in case it's useful.

I'm working with taking a third party's API and writing an OAS file I can use to generate clients. Some of the fields in the API are nullable, including fields that normally return arrays of objects.

This nullable string property defined in OAS gets the following type-assertion block at the top of its python class:

notes:
    oneOf:
        - type: string
          description: Notes textfield
          example: "Example notes attached to entity"
        - type: 'null'
          description: Null of no notes are set.
GETEXAMPLECALLS200RESPONSENOTES_ONE_OF_SCHEMAS = ["object", "str"]
...
# data type: str
oneof_schema_1_validator: Optional[StrictStr] = Field(default=None, description="Notes textfield")
# data type: object
oneof_schema_2_validator: Optional[Any] = Field(default=None, description="Null of no notes are set.")
actual_instance: Optional[Union[object, str]] = None
one_of_schemas: List[str] = Field(default=Literal["object", "str"])

Meanwhile, this nullable array of objects property defined in OAS gets the following type-assertion block at the top of its python class:

contents:
    oneOf:
        - type: array
          description: Array of content objects
          items:
            type: object
            description: Content object
            properties:
                row-id:
                    type: integer
                    description: Row number inside table
                    example: 1
                name:
                    type: string
                    description: Name of content
                    example: "Demo file"
        - type: 'null'
          description: Null of no notes are set.
GETEXAMPLECALLS200RESPONSECONTENTS_ONE_OF_SCHEMAS = ["object", "str"]
...
# data type: object
oneof_schema_1_validator: Optional[Any] = Field(default=None, description="Array of content objects")
# data type: object
oneof_schema_2_validator: Optional[Any] = Field(default=None, description="Null if no contents are set.")
actual_instance: Optional[Union[object]] = None
one_of_schemas: List[str] = Field(default=Literal["object"])

As a result, when I make an API call when contents: is not null, I get the error:

  Value error, Multiple matches found when setting `actual_instance` in GetExampleCalls200ResponseContents
  with oneOf schemas: object. Details:  [type=value_error, input_value=[{'row-id': 1, 'name': 'Demo file'}]
  input_type=list]
    For further information visit https://errors.pydantic.dev/2.6/v/value_error

Hit here because the result is matching true against both oneof_schema_1_validator and oneof_schema_2_validator.

Is there something I can do with the spec to clarify the differences between contents:'s oneOfs? Also reporting this as a bug in case oneof_schema_1_validator should be checking against Optional[List] instead of Optional[Any].

openapi-generator version

➜  npx --version
10.4.0
➜  npx @openapitools/openapi-generator-cli version
7.3.0

OpenAPI declaration file content or url

Here's a full small sample spec that can generate the issue
```yaml openapi: 3.1.0 info: description: Example call to demo python generator version: v1 title: Python example contact: name: Example email: example@email.com url: https://example.com/ license: name: Example url: https://www.example.com/ tags: - name: ExampleCall description: Example Call Tag servers: - url: https://example.com/ paths: /api/v1/example: get: summary: Example call to demo python generator description: Example call to demo python generator security: - BearerAuth: [] tags: - ExampleCall operationId: getExampleCalls responses: "200": description: Successfully fetched items. content: application/json: schema: type: object properties: title: type: string description: Title example: "Example title" description: type: string description: Description example: "Example description" notes: oneOf: - type: string description: Notes textfield example: "Example notes attached to entity" - type: 'null' description: Null of no notes are set. contents: oneOf: - type: array description: Array of content objects items: type: object description: Content object properties: row-id: type: integer description: Row number inside table example: 1 name: type: string description: Name of content example: "Demo file" - type: 'null' description: Null if no contents are set. components: securitySchemes: BearerAuth: type: http scheme: bearer ```

Generation Details

NPM CLI generation

➜  npx @openapitools/openapi-generator-cli generate -i smallexample.yaml -g python -o example-python-client

Latest snapshot generation (from today, Feb 23)

➜  java -jar openapi-generator-cli-7.4.0-20240223.080829-40.jar generate -i smallexample.yaml -g python -o example-python-client-2

Steps to reproduce

I am not able to make the API I'm using available for this bug, but the payload returning from the API call is:

{
    "title": "Example title",
    "description": "Example description",
    "notes": null,
    "contents": [
        {
            "row-id": 1,
            "name": "Demo file"
        }
    ]
}

Setting up a test where the ExampleCall is mocked to return this output would reproduce the same error:

  Value error, Multiple matches found when setting `actual_instance` in GetExampleCalls200ResponseContents
  with oneOf schemas: object. Details:  [type=value_error, input_value=[{'row-id': 1, 'name': 'Demo file'}]
  input_type=list]
    For further information visit https://errors.pydantic.dev/2.6/v/value_error

Related issues/PRs

Mar 27, 2023 - This issue is around oneOf object and list(object) for the python-nextgen generator, which is kinda what the issue here is. I am not sure what command that user ran to build their client. python-nextgen is not a generator name, and I think python as the client generator is the latest version?

No other issues within the past year tagged for Python seem to be related.

Suggest a fix

🟢 The workaround

Here's an alternate version of the OAS file where `oneOf`s are replaced with:
```yaml openapi: 3.1.0 info: description: Python OAS Client Generator Demos version: v1 title: Python example contact: name: Example email: example@email.com url: https://dummyurl.com license: name: Example url: https://www.dummyurl.com tags: - name: ExampleCall description: Example Call Tag servers: - url: https://dummyurl.com paths: /api/v1/example: get: summary: Example call to demo python generator description: This is an example to call to demo python generator security: - BearerAuth: [] tags: - ExampleCall operationId: getExampleCalls responses: "200": description: Successfully fetched items. content: application/json: schema: type: object properties: title: type: string description: Title example: "Example title" helpertext: type: string description: Description example: "Example description" notes: type: - string - 'null' description: Notes textfield. Null of no notes are set. example: "Example notes attached to entity" contents: type: - array - 'null' items: type: object description: Example of an inner object returned inside an array. Null if no contents are set. properties: row-id: type: integer description: Row number inside table example: 1 name: type: string description: Name of content example: "Demo file" example: row-id: 1 name: Demo file example: - row-id: 1 name: Demo file - row-id: 2 name: Another file example: title: Example title 1 helpertext: Example description 1 notes: Example notes contents: - row-id: 1 name: Demo file - row-id: 2 name: Another file examples: fullResult: value: title: Example title 1 helpertext: Example description 1 notes: Example notes contents: - row-id: 1 name: Demo file - row-id: 2 name: Another file blankArrayResult: summary: All fields are populated value: title: Example title 2 helpertext: Example description 2 notes: ~ contents: [] nullResult: summary: All fields are null value: title: Example title 3 helpertext: Example description 3 notes: ~ contents: ~ "404": description: API called incorrectly. content: application/json: schema: type: object properties: error: type: string description: Error message text example: "Error encountered" example: error: Error encountered examples: standardResult: summary: Usual result value: error: Error encountered components: securitySchemes: BearerAuth: type: http description: access_token after calling /connect/token endpoint. scheme: bearer ```
type:
  - array
  - 'null'

The resulting client generated in the same steps outlined above does create that List[Object] syntax assertion:

title: Optional[StrictStr] = Field(default=None, description="Title")
helpertext: Optional[StrictStr] = Field(default=None, description="Description")
notes: Optional[StrictStr] = Field(default=None, description="Notes textfield. Null of no notes are set.")
contents: Optional[List[GetExampleCalls200ResponseContentsInner]] = None
__properties: ClassVar[List[str]] = ["title", "helpertext", "notes", "contents"]

So hypothetically, the handling exists already. But maybe the way that oneOf is set up in the structure makes it difficult to use in the same way as just the multiple type:s.

kdipippo commented 7 months ago

Confirming that switching from:

ExampleField:
  type:
    oneOf:
      - type: 'null'
        description: This field is null when A, B, C
        example: null
      - type: string
        description: Example string field.
        example: "example"

to

ExampleField:
  type:
    - string
    - 'null'
  description: String when A, B, C. Null when X, Y, Z
  example: "example"

Resulted in the client just generating the line: example_field: Optional[StrictStr] = Field(default=None, description="String when A, B, C. Null when X, Y, Z", alias="ExampleField") With none of the alternate-schemas code generated. This model is able to house the API responses.