openapi-generator icon indicating copy to clipboard operation
openapi-generator copied to clipboard

[BUG][Python] Wrapper models for oneOf values include metadata fields in serialized responses

Open claudiadpp opened this issue 5 months ago • 2 comments

Bug Report Checklist

  • [ ] Have you provided a full/minimal spec to reproduce the issue?
  • [ ] Have you validated the input using an OpenAPI validator?
  • [x] Have you tested with the latest master to confirm the issue still exists?
  • [x] Have you searched for related issues/PRs?
  • [ ] What's the actual output vs expected output?
  • [ ] [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

When using the python-pydantic-v2 generator, defining a schema with additionalProperties whose values are oneOf produces an intermediate wrapper model for each dictionary value.

For the API definition below, the generator produces for followingApiResponse class:

class ApiResponse(BaseModel):
    """
    ApiResponse
    """ # noqa: E501
    message: StrictStr
    extra: Optional[Dict[str, ApiResponseExtraValue]] = Field(default=None, description="Map of extras")
    __properties: ClassVar[List[str]] = ["message", "extra"]

where the ApiResponseExtraValue class is generated as:

APIRESPONSEEXTRAVALUE_ONE_OF_SCHEMAS = ["ExtraAlpha", "ExtraBeta"]

class ApiResponseExtraValue(BaseModel):
    """
    ApiResponseExtraValue
    """
    # data type: ExtraAlpha
    oneof_schema_1_validator: Optional[ExtraAlpha] = None
    # data type: ExtraBeta
    oneof_schema_2_validator: Optional[ExtraBeta] = None
    actual_instance: Optional[Union[ExtraAlpha, ExtraBeta]] = None
    one_of_schemas: List[str] = Literal["ExtraAlpha", "ExtraBeta"]

    model_config = {
        "validate_assignment": True,
        "protected_namespaces": (),
    }

Returning a ApiResponse currently serializes all fields of the wrapper model, resulting in responses like:

{
    "oneof_schema_1_validator": null,
    "oneof_schema_2_validator": null,
    "actual_instance": {
        "type": "beta",
        "variant": "excel",
        "label": "Descargar",
        "downloadId": "68b84d830ed1dd568216e27a"
    },
    "one_of_schemas": ["ExtraButton", "ExtraTable"]
}

The desired output is only the actual payload:

{
    "type": "beta",
    "variant": "excel",
    "label": "Descargar",
    "downloadId": "68b84d830ed1dd568216e27a"
}

The generator creates a wrapper model for the extra property which was defined with oneOf value. Wrapper fields like oneof_schema_1_validator, oneof_schema_2_validator, and one_of_schemas are metadata for the generator, not real API data. The generated code includes these fields in instance serialization, so FastAPI returns them in responses.

openapi-generator version

openapi-generator v7.15.0 (latest at the moment) generator: python-fastapi (pydantic v2) pydantic version: 2.11.7 (latest at the moment) fastapi version: 0.116.1 (latest at the moment)

OpenAPI declaration file content or url
openapi: 3.0.3
info:
  title: Literal Bug Example
  version: 1.0.0
paths: {}
components:
  schemas:
    ApiResponse:
      type: object
      additionalProperties: false
      properties:
        message:
          type: string
          example: "Hello world"
        extra:
          description: "Map of extras"
          type: object
          additionalProperties:
            oneOf:
              - $ref: '#/components/schemas/ExtraAlpha'
              - $ref: '#/components/schemas/ExtraBeta'
      required:
        - message

    ExtraAlpha:
      type: object
      required: [type, foo]
      properties:
        type:
          type: string
          enum: ["alpha"]
        foo:
          type: string

    ExtraBeta:
      type: object
      required: [type, variant, label, url]
      properties:
        type:
          type: string
          enum: ["beta"]
        variant:
          type: string
          enum: ["excel"]
        label:
          type: string
          maxLength: 40
        downloadId:
          type: string
          maxLength: 50
Generation Details
Steps to reproduce
Related issues/PRs
Suggest a fix

Manually converting metadata fields to ClassVar can remove them from the serialized response:

class ApiResponseExtraValue(BaseModel):
    """
    ApiResponseExtraValue
    """
    # data type: ExtraAlpha
    oneof_schema_1_validator: ClassVar[Optional[ExtraAlpha]] = None
    # data type: ExtraBeta
    oneof_schema_2_validator: ClassVar[Optional[ExtraBeta]] = None
    actual_instance: Optional[Union[ExtraAlpha, ExtraBeta]] = None
    one_of_schemas: ClassVar[List[str]] = Literal["ExtraAlpha", "ExtraBeta"]

    model_config = {
        "validate_assignment": True,
        "protected_namespaces": (),
    }

However, the actual_instance wrapper is still serialized:

{
    "actual_instance": {
        "type": "beta",
        "variant": "excel",
        "label": "Descargar",
        "downloadId": "68b943b1bb9793a88d289545"
    }
}

when the response should be

{
    "type": "download",
    "variant": "excel",
    "label": "Descargar",
    "downloadId": "68b84d830ed1dd568216e27a"
}

The suggested fix is:

  • Only serialize the actual payload (actual_instance) when returning the model.
  • Treat the wrapper fields (oneof_schema_1_validator, oneof_schema_2_validator, and one_of_schemas) as class-only metadata or remove them entirely.

claudiadpp avatar Sep 04 '25 09:09 claudiadpp

I am facing a similar issue:

🐞 Bug Report / Feature Request: Improve oneOf Handling for FastAPI/Pydantic Summary: The current OpenAPI Generator implementation for Python (FastAPI + Pydantic) generates wrapper classes for oneOf schemas (e.g., CaseEmbeddingsInputPayloadCaseInformation) that rely on a field like actual_instance and custom deserialization logic. However, this structure is incompatible with FastAPI's request parsing flow and Pydantic's native validation pipeline, leading to broken or non-functional schema resolution.

{
  "title": "Case Embeddings Input",
  "type": "object",
  "required": ["case_information"],
  "properties": {
    "case_information": {
      "oneOf": [
        {
          "$ref": "./case_information_with_case_number.json"
        },
        {
          "$ref": "./case_information_with_id_v2.json"
        }
      ]
    }
  }
}

and my individual schemas are:

{
  "title": "Case Information with case number",
  "type": "object",
  "required": [
    "partner_id",
    "case_number",
    "case_category",
    "case_description"
  ],
  "properties": {
    "partner_id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 30,
      "description": "Partner Id"
    },
    "case_number": {
      "type": "string",
      "minLength": 1,
      "maxLength": 30,
      "description": "Case Id"
    },
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 30,
      "description": "Case Id",
      "nullable": true
    },
    "case_category": {
      "type": "string",
      "minLength": 1,
      "maxLength": 5000,
      "description": "Case Category"
    },
    "case_description": {
      "type": "string",
      "minLength": 1,
      "maxLength": 5000,
      "description": "Case Description"
    },
    "case_language_identifier": {
      "type": "string",
      "minLength": 1,
      "maxLength": 10,
      "description": "Case Language Identifier"
    }
  }
}

Schema 2

{
  "title": "Case Information with id",
  "type": "object",
  "required": [
    "partner_id",
    "id",
    "case_category",
    "case_description"
  ],
  "properties": {
    "partner_id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 30,
      "description": "Partner Id"
    },
    "case_number": {
      "type": "string",
      "minLength": 1,
      "maxLength": 30,
      "description": "Case Id",
      "nullable": true
    },
    "id": {
      "type": "string",
      "minLength": 1,
      "maxLength": 30,
      "description": "Case Id"
    },
    "case_category": {
      "type": "string",
      "minLength": 1,
      "maxLength": 5000,
      "description": "Case Category"
    },
    "case_description": {
      "type": "string",
      "minLength": 1,
      "maxLength": 5000,
      "description": "Case Description"
    },
    "case_language_identifier": {
      "type": "string",
      "minLength": 1,
      "maxLength": 10,
      "description": "Case Language Identifier"
    }
  }
}

which produces

class CaseEmbeddingsInputPayload(BaseModel):
    case_information: Optional[CaseEmbeddingsInputPayloadCaseInformation] = None

Where CaseEmbeddingsInputPayloadCaseInformation wraps the oneOf logic using an actual_instance field and custom init() or from_json() methods. However:

  • FastAPI does not invoke init() or from_json() during request parsing.
  • The payload is passed as keyword arguments, which do not match the expected structure.
  • As a result, actual_instance is never set, and validation fails silently. unless we manually set {"actual_instance": **kwargs"} in the init def.
  • The input ends up being None, even though it matches one of the schemas.

Instead of generating a wrapper class, emit a native Python Union type:

class CaseEmbeddingsInputPayload(BaseModel):
    case_information: Union[CaseInformationWithCaseNumber, CaseInformationWithIdV2]

which works.

lek18 avatar Sep 17 '25 16:09 lek18

Instead of generating a wrapper class, emit a native Python Union type

exactly. 👍

I have seen the same problem in my project and going to override the auto-generated models with simple Union types.

ShedPlant avatar Dec 03 '25 15:12 ShedPlant