-
-
Notifications
You must be signed in to change notification settings - Fork 7.3k
Open
Labels
Description
Bug Report Checklist
- Have you provided a full/minimal spec to reproduce the issue?
- Have you validated the input using an OpenAPI validator?
- Have you tested with the latest master to confirm the issue still exists?
- Have you searched for related issues/PRs?
- What's the actual output vs expected output?
Description
anyOf seems to be broken for the cases from the description:
components:
schemas:
CreateItemRequest:
type: object
required:
- items
properties:
items:
type: array
items:
$ref: '#/components/schemas/CreateItem'
CreateItem:
anyOf:
- $ref: '#/components/schemas/CreateItemA'
- $ref: '#/components/schemas/CreateItemB'
discriminator:
propertyName: type
mapping:
a: '#/components/schemas/CreateItemA'
b: '#/components/schemas/CreateItemB'
CreateItemA:
type: object
title: Create Item A
properties:
type:
type: string
default: a
a:
type: string
required:
- type
CreateItemB:
type: object
title: Create Item B
properties:
type:
type: string
default: b
b:
type: string
required:
- type
- bCorrect me if it is a BUG or if I am using the generated SDK in a wrong way.
request = CreateItemRequest(recipients=[CreateItem(type="a"), CreateItem(type="b", b="b")])- Expected result for
CreateItemRequestshould be like:
request.recipients[0].model_dump()
# {"type": "a"}
request.recipients[1].model_dump()
# {"type": "b", "b": "b"}- Actual result for
CreateItemRequest:
request.recipients[0].model_dump()
{'anyof_schema_1_validator': None, 'anyof_schema_2_validator': None, 'actual_instance': None, 'any_of_schemas': {'CreateItemA', 'CreateItemB'}, 'discriminator_value_class_map': {}}
request.recipients[1].model_dump()
{'anyof_schema_1_validator': None, 'anyof_schema_2_validator': None, 'actual_instance': None, 'any_of_schemas': {'CreateItemA', 'CreateItemB'}, 'discriminator_value_class_map': {}}openapi-generator version
v7.16.0
OpenAPI declaration file content
openapi: 3.0.3
info:
title: Public API
version: 1.0.0
paths:
/path:
post:
summary: Create
operationId: createItems
description: Create items
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateItemRequest'
responses:
'200':
description: OK
components:
schemas:
CreateItemRequest:
type: object
required:
- items
properties:
items:
type: array
items:
$ref: '#/components/schemas/CreateItem'
CreateItem:
anyOf:
- $ref: '#/components/schemas/CreateItemA'
- $ref: '#/components/schemas/CreateItemB'
discriminator:
propertyName: type
mapping:
a: '#/components/schemas/CreateItemA'
b: '#/components/schemas/CreateItemB'
CreateItemA:
type: object
title: Create Item A
properties:
type:
type: string
default: a
a:
type: string
required:
- type
CreateItemB:
type: object
title: Create Item B
properties:
type:
type: string
default: b
b:
type: string
required:
- type
- bGeneration Details
# config.yaml
gitRepoId: 'api-python-client'
gitUserId: 'Test'
packageName: 'test_client'
httpUserAgent: 'test_client/1.0.0'
additionalProperties:
packageVersion: '1.0.0'
projectName: 'test-client'
infoName: 'test'
Steps to reproduce
docker run --rm -v "${PWD}:/local" openapitools/openapi-generator-cli:v7.16.0 generate -i /local/openapi.yaml -g python -o /local/test_client
Results in:
# test_client/api/default_api.py
...
class DefaultApi:
...
@validate_call
def create_items(
self,
create_item_request: CreateItemRequest,
_request_timeout: Union[
...# test_client/models/create_item_request.py
class CreateItemRequest(BaseModel):
"""
CreateItemRequest
""" # noqa: E501
items: List[CreateItem]
__properties: ClassVar[List[str]] = ["items"]This part requires energy to dive into
# test_client/models/create_item.py
CREATEITEM_ANY_OF_SCHEMAS = ["CreateItemA", "CreateItemB"]
class CreateItem(BaseModel):
"""
CreateItem
"""
# data type: CreateItemA
anyof_schema_1_validator: Optional[CreateItemA] = None
# data type: CreateItemB
anyof_schema_2_validator: Optional[CreateItemB] = None
if TYPE_CHECKING:
actual_instance: Optional[Union[CreateItemA, CreateItemB]] = None
else:
actual_instance: Any = None
any_of_schemas: Set[str] = { "CreateItemA", "CreateItemB" }
model_config = {
"validate_assignment": True,
"protected_namespaces": (),
}
discriminator_value_class_map: Dict[str, str] = {
}
def __init__(self, *args, **kwargs) -> None:
if args:
if len(args) > 1:
raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`")
if kwargs:
raise ValueError("If a position argument is used, keyword arguments cannot be used.")
super().__init__(actual_instance=args[0])
else:
super().__init__(**kwargs)
@field_validator('actual_instance')
def actual_instance_must_validate_anyof(cls, v):
instance = CreateItem.model_construct()
error_messages = []
# validate data type: CreateItemA
if not isinstance(v, CreateItemA):
error_messages.append(f"Error! Input type `{type(v)}` is not `CreateItemA`")
else:
return v
# validate data type: CreateItemB
if not isinstance(v, CreateItemB):
error_messages.append(f"Error! Input type `{type(v)}` is not `CreateItemB`")
else:
return v
if error_messages:
# no match
raise ValueError("No match found when setting the actual_instance in CreateItem with anyOf schemas: CreateItemA, CreateItemB. Details: " + ", ".join(error_messages))
else:
return v
@classmethod
def from_dict(cls, obj: Dict[str, Any]) -> Self:
return cls.from_json(json.dumps(obj))
@classmethod
def from_json(cls, json_str: str) -> Self:
"""Returns the object represented by the json string"""
instance = cls.model_construct()
error_messages = []
# anyof_schema_1_validator: Optional[CreateItemA] = None
try:
instance.actual_instance = CreateItemA.from_json(json_str)
return instance
except (ValidationError, ValueError) as e:
error_messages.append(str(e))
# anyof_schema_2_validator: Optional[CreateItemB] = None
try:
instance.actual_instance = CreateItemB.from_json(json_str)
return instance
except (ValidationError, ValueError) as e:
error_messages.append(str(e))
if error_messages:
# no match
raise ValueError("No match found when deserializing the JSON string into CreateItem with anyOf schemas: CreateItemA, CreateItemB. Details: " + ", ".join(error_messages))
else:
return instance
def to_json(self) -> str:
"""Returns the JSON representation of the actual instance"""
if self.actual_instance is None:
return "null"
if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json):
return self.actual_instance.to_json()
else:
return json.dumps(self.actual_instance)
def to_dict(self) -> Optional[Union[Dict[str, Any], CreateItemA, CreateItemB]]:
"""Returns the dict representation of the actual instance"""
if self.actual_instance is None:
return None
if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict):
return self.actual_instance.to_dict()
else:
return self.actual_instance
def to_str(self) -> str:
"""Returns the string representation of the actual instance"""
return pprint.pformat(self.model_dump())# test_client/models/create_item_a.py
class CreateItemA(BaseModel):
"""
CreateItemA
""" # noqa: E501
type: StrictStr
a: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["type", "a"]# test_client/models/create_item_b.py
class CreateItemB(BaseModel):
"""
CreateItemB
""" # noqa: E501
type: StrictStr
b: StrictStr
__properties: ClassVar[List[str]] = ["type", "b"]Suggest a fix
Pydantic RootModel / Literal approach should be used instead
# test_client/models/create_item.py
class CreateItem(
RootModel[Union[CreateItemA, CreateItemB]]
):
root: Union[CreateItemA, CreateItemB] = Field(
..., discriminator="type"
)
def to_dict(self) -> Optional[Union[Dict[str, Any], CreateItemA, CreateItemB]]:
"""Returns the dict representation of the actual instance"""
return self.model_dump()# test_client/models/create_item_a.py
class CreateItemA(BaseModel):
"""
CreateItemA
""" # noqa: E501
type: Literal["a"] = Field(..., examples=["a"])
a: Optional[StrictStr] = None# test_client/models/create_item_b.py
class CreateItemB(BaseModel):
"""
CreateItemB
""" # noqa: E501
type: Literal["b"] = Field(..., examples=["b"])
b: StrictStrfrom test_client.models import CreateItemRequest, CreateItem
request = CreateItemRequest(items=[CreateItem(type="a"), CreateItem(type="b", b="b")])
request.items[0].model_dump()
# {'type': 'a', 'a': None}
request.items[1].model_dump()
# {'type': 'b', 'b': 'b'}
request = CreateItemRequest(items=[dict(type="a"), dict(type="b", b="b")])
request.items[0].model_dump()
# {'type': 'a', 'a': None}
>>> request.items[1].model_dump()
# {'type': 'b', 'b': 'b'}===
Tested against local mock server [ fatapi, uvicorn ]:
# mock_server.py
from fastapi import FastAPI
import uvicorn
app = FastAPI(title="PandaDoc Mock API", version="1.0.0")
@app.post("/path")
async def path(payload: dict = None):
assert payload == {'items': [{'type': 'a', 'a': None}, {'type': 'b', 'b': 'b'}]}
print(f"\t ✅ {payload}")
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)$ python mock_server.py
INFO: Started server process [99687]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
✅ {'items': [{'type': 'a', 'a': None}, {'type': 'b', 'b': 'b'}]}
INFO: 127.0.0.1:53587 - "POST /path HTTP/1.1" 200 OK
import test_client
from test_client.models.create_item_request import CreateItemRequest, CreateItem
configuration = test_client.Configuration(
host = "http://localhost:8000"
)
with test_client.ApiClient(configuration) as api_client:
api_instance = test_client.DefaultApi(api_client)
create_item_request = test_client.CreateItemRequest(items=[CreateItem(type="a"), CreateItem(type="b", b="b")])
api_instance.create_items(create_item_request)
# debug
print(create_item_request.model_dump()){'items': [{'type': 'a', 'a': None}, {'type': 'b', 'b': 'b'}]}
ShedPlant