From 59d96a3ca1b14c1b621ddf3bd25e16948528025a Mon Sep 17 00:00:00 2001 From: Max Goltzsche Date: Wed, 31 Jul 2024 02:01:12 +0200 Subject: [PATCH 1/4] [python-fastapi] dont inherit additionalProperties Don't let models inherit the value type of additionalProperties and arrays. This is to fix a bug where the `python-fastapi` server generator generated invalid models that inherited the value type specified within additionalProperties. --- .../languages/PythonFastAPIServerCodegen.java | 5 ++++ .../python/PythonFastapiCodegenTest.java | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java index 802cd4353485..0b95d81e8fbf 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonFastAPIServerCodegen.java @@ -140,6 +140,11 @@ public PythonFastAPIServerCodegen() { .defaultValue(implPackage)); } + @Override + protected void addParentFromContainer(CodegenModel model, Schema schema) { + // we do not want to inherit simply because additionalProperties is set + } + @Override public void processOpts() { super.processOpts(); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java index 9a222ac9fae3..c4d880e71d37 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java @@ -1,9 +1,17 @@ package org.openapitools.codegen.python; +import com.google.common.collect.Sets; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Schema; import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.DefaultCodegen; import org.openapitools.codegen.DefaultGenerator; import org.openapitools.codegen.TestUtils; import org.openapitools.codegen.config.CodegenConfigurator; +import org.openapitools.codegen.languages.PythonClientCodegen; +import org.testng.Assert; import org.testng.annotations.Test; import java.io.File; @@ -53,4 +61,24 @@ public void testEndpointSpecsWithoutDescription() throws IOException { TestUtils.assertFileContains(Paths.get(output + "/src/nodesc/apis/desc_api.py"), "return await BaseDescApi.subclasses[0]().desc()\n"); } + + @Test(description = "additionalProperties should not let container type inherit their type") + public void additionalPropertiesModelTest() { + final Schema model = new ArraySchema() + //.description() + .items(new Schema().type("object").additionalProperties(new Schema().type("string"))) + .description("model with additionalProperties"); + final DefaultCodegen codegen = new PythonClientCodegen(); + OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", model); + codegen.setOpenAPI(openAPI); + final CodegenModel cm = codegen.fromModel("sample", model); + + Assert.assertEquals(cm.name, "sample"); + Assert.assertEquals(cm.classname, "Sample"); + Assert.assertEquals(cm.description, "model with additionalProperties"); + Assert.assertEquals(cm.vars.size(), 0); + Assert.assertEquals(cm.parent, "null"); + Assert.assertEquals(cm.imports.size(), 0); + Assert.assertEquals(Sets.intersection(cm.imports, Sets.newHashSet()).size(), 0); + } } From 61ab83a32785e02033e69f487efe6ea62a8a1b22 Mon Sep 17 00:00:00 2001 From: Max Goltzsche Date: Mon, 5 Aug 2024 23:27:29 +0200 Subject: [PATCH 2/4] [python-fastapi] support pydantic v2 models Previously, the generated `additional_properties` field showed up within the response of the generated API as opposed marshaling the model so that its fields are added to the root object. Apparently that is because pydantic v2 does not honour the generated `to_dict` methods anymore (which would have mapped the object to the correct representation) but, instead, supports additional properties natively by specifying `extra=allow` within the `model_config`. Correspondingly, the following changes have been applied: * To allow additional fields, specify `extra=allow` within the `model_config`. * Don't generate the `additional_properties` field - users can use pydantic's built-in `model.extra_fields` instead. * Let the `{to|from}_{dict|json}` methods delegate to Pydantic's `model_dump[_json]` methods. --- .../python-fastapi/model_generic.mustache | 274 +----------------- .../src/openapi_server/models/api_response.py | 28 +- .../src/openapi_server/models/category.py | 27 +- .../src/openapi_server/models/order.py | 31 +- .../src/openapi_server/models/pet.py | 41 +-- .../src/openapi_server/models/tag.py | 27 +- .../src/openapi_server/models/user.py | 33 +-- 7 files changed, 31 insertions(+), 430 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/python-fastapi/model_generic.mustache b/modules/openapi-generator/src/main/resources/python-fastapi/model_generic.mustache index 41354ee62410..4637734528c5 100644 --- a/modules/openapi-generator/src/main/resources/python-fastapi/model_generic.mustache +++ b/modules/openapi-generator/src/main/resources/python-fastapi/model_generic.mustache @@ -24,9 +24,6 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{#vars}} {{name}}: {{{vendorExtensions.x-py-typing}}} {{/vars}} -{{#isAdditionalPropertiesTrue}} - additional_properties: Dict[str, Any] = {} -{{/isAdditionalPropertiesTrue}} __properties: ClassVar[List[str]] = [{{#allVars}}"{{baseName}}"{{^-last}}, {{/-last}}{{/allVars}}] {{#vars}} {{#vendorExtensions.x-regex}} @@ -84,38 +81,19 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} "populate_by_name": True, "validate_assignment": True, "protected_namespaces": (), +{{#isAdditionalPropertiesTrue}} + "extra": "allow", +{{/isAdditionalPropertiesTrue}} } -{{#hasChildren}} -{{#discriminator}} - # JSON field name that stores the object type - __discriminator_property_name: ClassVar[List[str]] = '{{discriminator.propertyBaseName}}' - - # discriminator mappings - __discriminator_value_class_map: ClassVar[Dict[str, str]] = { - {{#mappedModels}}'{{{mappingName}}}': '{{{modelName}}}'{{^-last}},{{/-last}}{{/mappedModels}} - } - - @classmethod - def get_discriminator_value(cls, obj: Dict) -> str: - """Returns the discriminator value (object type) of the data""" - discriminator_value = obj[cls.__discriminator_property_name] - if discriminator_value: - return cls.__discriminator_value_class_map.get(discriminator_value) - else: - return None - -{{/discriminator}} -{{/hasChildren}} def to_str(self) -> str: """Returns the string representation of the model using alias""" return pprint.pformat(self.model_dump(by_alias=True)) def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return self.model_dump_json(by_alias=True, exclude_unset=True) @classmethod def from_json(cls, json_str: str) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}: @@ -123,257 +101,19 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} return cls.from_dict(json.loads(json_str)) def to_dict(self) -> Dict[str, Any]: - """Return the dictionary representation of the model using alias. - - This has the following differences from calling pydantic's - `self.model_dump(by_alias=True)`: - - * `None` is only added to the output dict for nullable fields that - were set at model initialization. Other fields with value `None` - are ignored. - {{#vendorExtensions.x-py-readonly}} - * OpenAPI `readOnly` fields are excluded. - {{/vendorExtensions.x-py-readonly}} - {{#isAdditionalPropertiesTrue}} - * Fields in `self.additional_properties` are added to the output dict. - {{/isAdditionalPropertiesTrue}} - """ - _dict = self.model_dump( - by_alias=True, - exclude={ - {{#vendorExtensions.x-py-readonly}} - "{{{.}}}", - {{/vendorExtensions.x-py-readonly}} - {{#isAdditionalPropertiesTrue}} - "additional_properties", - {{/isAdditionalPropertiesTrue}} - }, - exclude_none=True, - ) - {{#allVars}} - {{#isContainer}} - {{#isArray}} - {{#items.isArray}} - {{^items.items.isPrimitiveType}} - # override the default output from pydantic by calling `to_dict()` of each item in {{{name}}} (list of list) - _items = [] - if self.{{{name}}}: - for _item in self.{{{name}}}: - if _item: - _items.append( - [_inner_item.to_dict() for _inner_item in _item if _inner_item is not None] - ) - _dict['{{{baseName}}}'] = _items - {{/items.items.isPrimitiveType}} - {{/items.isArray}} - {{^items.isArray}} - {{^items.isPrimitiveType}} - {{^items.isEnumOrRef}} - # override the default output from pydantic by calling `to_dict()` of each item in {{{name}}} (list) - _items = [] - if self.{{{name}}}: - for _item in self.{{{name}}}: - if _item: - _items.append(_item.to_dict()) - _dict['{{{baseName}}}'] = _items - {{/items.isEnumOrRef}} - {{/items.isPrimitiveType}} - {{/items.isArray}} - {{/isArray}} - {{#isMap}} - {{#items.isArray}} - {{^items.items.isPrimitiveType}} - # override the default output from pydantic by calling `to_dict()` of each value in {{{name}}} (dict of array) - _field_dict_of_array = {} - if self.{{{name}}}: - for _key in self.{{{name}}}: - if self.{{{name}}}[_key] is not None: - _field_dict_of_array[_key] = [ - _item.to_dict() for _item in self.{{{name}}}[_key] - ] - _dict['{{{baseName}}}'] = _field_dict_of_array - {{/items.items.isPrimitiveType}} - {{/items.isArray}} - {{^items.isArray}} - {{^items.isPrimitiveType}} - {{^items.isEnumOrRef}} - # override the default output from pydantic by calling `to_dict()` of each value in {{{name}}} (dict) - _field_dict = {} - if self.{{{name}}}: - for _key in self.{{{name}}}: - if self.{{{name}}}[_key]: - _field_dict[_key] = self.{{{name}}}[_key].to_dict() - _dict['{{{baseName}}}'] = _field_dict - {{/items.isEnumOrRef}} - {{/items.isPrimitiveType}} - {{/items.isArray}} - {{/isMap}} - {{/isContainer}} - {{^isContainer}} - {{^isPrimitiveType}} - {{^isEnumOrRef}} - # override the default output from pydantic by calling `to_dict()` of {{{name}}} - if self.{{{name}}}: - _dict['{{{baseName}}}'] = self.{{{name}}}.to_dict() - {{/isEnumOrRef}} - {{/isPrimitiveType}} - {{/isContainer}} - {{/allVars}} - {{#isAdditionalPropertiesTrue}} - # puts key-value pairs in additional_properties in the top level - if self.additional_properties is not None: - for _key, _value in self.additional_properties.items(): - _dict[_key] = _value - - {{/isAdditionalPropertiesTrue}} - {{#allVars}} - {{#isNullable}} - # set to None if {{{name}}} (nullable) is None - # and model_fields_set contains the field - if self.{{name}} is None and "{{{name}}}" in self.model_fields_set: - _dict['{{{baseName}}}'] = None - - {{/isNullable}} - {{/allVars}} - return _dict + """Return the dictionary representation of the model using alias""" + return self.model_dump(by_alias=True, exclude_unset=True) @classmethod def from_dict(cls, obj: Dict) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}: """Create an instance of {{{classname}}} from a dict""" - {{#hasChildren}} - {{#discriminator}} - # look up the object type based on discriminator mapping - object_type = cls.get_discriminator_value(obj) - if object_type: - klass = globals()[object_type] - return klass.from_dict(obj) - else: - raise ValueError("{{{classname}}} failed to lookup discriminator value from " + - json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name + - ", mapping: " + json.dumps(cls.__discriminator_value_class_map)) - {{/discriminator}} - {{/hasChildren}} - {{^hasChildren}} if obj is None: return None if not isinstance(obj, dict): return cls.model_validate(obj) - {{#disallowAdditionalPropertiesIfNotPresent}} - {{^isAdditionalPropertiesTrue}} - # raise errors for additional fields in the input - for _key in obj.keys(): - if _key not in cls.__properties: - raise ValueError("Error due to additional fields (not defined in {{classname}}) in the input: " + _key) - - {{/isAdditionalPropertiesTrue}} - {{/disallowAdditionalPropertiesIfNotPresent}} - _obj = cls.model_validate({ - {{#allVars}} - {{#isContainer}} - {{#isArray}} - {{#items.isArray}} - {{#items.items.isPrimitiveType}} - "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} - {{/items.items.isPrimitiveType}} - {{^items.items.isPrimitiveType}} - "{{{baseName}}}": [ - [{{{items.items.dataType}}}.from_dict(_inner_item) for _inner_item in _item] - for _item in obj.get("{{{baseName}}}") - ] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} - {{/items.items.isPrimitiveType}} - {{/items.isArray}} - {{^items.isArray}} - {{^items.isPrimitiveType}} - {{#items.isEnumOrRef}} - "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} - {{/items.isEnumOrRef}} - {{^items.isEnumOrRef}} - "{{{baseName}}}": [{{{items.dataType}}}.from_dict(_item) for _item in obj.get("{{{baseName}}}")] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} - {{/items.isEnumOrRef}} - {{/items.isPrimitiveType}} - {{#items.isPrimitiveType}} - "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} - {{/items.isPrimitiveType}} - {{/items.isArray}} - {{/isArray}} - {{#isMap}} - {{^items.isPrimitiveType}} - {{^items.isEnumOrRef}} - {{#items.isContainer}} - {{#items.isMap}} - "{{{baseName}}}": dict( - (_k, dict( - (_ik, {{{items.items.dataType}}}.from_dict(_iv)) - for _ik, _iv in _v.items() - ) - if _v is not None - else None - ) - for _k, _v in obj.get("{{{baseName}}}").items() - ) - if obj.get("{{{baseName}}}") is not None - else None{{^-last}},{{/-last}} - {{/items.isMap}} - {{#items.isArray}} - "{{{baseName}}}": dict( - (_k, - [{{{items.items.dataType}}}.from_dict(_item) for _item in _v] - if _v is not None - else None - ) - for _k, _v in obj.get("{{{baseName}}}").items() - ){{^-last}},{{/-last}} - {{/items.isArray}} - {{/items.isContainer}} - {{^items.isContainer}} - "{{{baseName}}}": dict( - (_k, {{{items.dataType}}}.from_dict(_v)) - for _k, _v in obj.get("{{{baseName}}}").items() - ) - if obj.get("{{{baseName}}}") is not None - else None{{^-last}},{{/-last}} - {{/items.isContainer}} - {{/items.isEnumOrRef}} - {{#items.isEnumOrRef}} - "{{{baseName}}}": dict((_k, _v) for _k, _v in obj.get("{{{baseName}}}").items()){{^-last}},{{/-last}} - {{/items.isEnumOrRef}} - {{/items.isPrimitiveType}} - {{#items.isPrimitiveType}} - "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} - {{/items.isPrimitiveType}} - {{/isMap}} - {{/isContainer}} - {{^isContainer}} - {{^isPrimitiveType}} - {{^isEnumOrRef}} - "{{{baseName}}}": {{{dataType}}}.from_dict(obj.get("{{{baseName}}}")) if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}} - {{/isEnumOrRef}} - {{#isEnumOrRef}} - "{{{baseName}}}": obj.get("{{{baseName}}}"){{#defaultValue}} if obj.get("{{baseName}}") is not None else {{defaultValue}}{{/defaultValue}}{{^-last}},{{/-last}} - {{/isEnumOrRef}} - {{/isPrimitiveType}} - {{#isPrimitiveType}} - {{#defaultValue}} - "{{{baseName}}}": obj.get("{{{baseName}}}") if obj.get("{{{baseName}}}") is not None else {{{defaultValue}}}{{^-last}},{{/-last}} - {{/defaultValue}} - {{^defaultValue}} - "{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}} - {{/defaultValue}} - {{/isPrimitiveType}} - {{/isContainer}} - {{/allVars}} - }) - {{#isAdditionalPropertiesTrue}} - # store additional fields in additional_properties - for _key in obj.keys(): - if _key not in cls.__properties: - _obj.additional_properties[_key] = obj.get(_key) - - {{/isAdditionalPropertiesTrue}} - return _obj - {{/hasChildren}} + return cls.parse_obj(obj) {{#vendorExtensions.x-py-postponed-model-imports.size}} {{#vendorExtensions.x-py-postponed-model-imports}} diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/api_response.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/api_response.py index 441c3b825096..1d6fe59b6050 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/api_response.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/api_response.py @@ -49,8 +49,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return self.model_dump_json(by_alias=True, exclude_unset=True) @classmethod def from_json(cls, json_str: str) -> Self: @@ -58,22 +57,8 @@ def from_json(cls, json_str: str) -> Self: return cls.from_dict(json.loads(json_str)) def to_dict(self) -> Dict[str, Any]: - """Return the dictionary representation of the model using alias. - - This has the following differences from calling pydantic's - `self.model_dump(by_alias=True)`: - - * `None` is only added to the output dict for nullable fields that - were set at model initialization. Other fields with value `None` - are ignored. - """ - _dict = self.model_dump( - by_alias=True, - exclude={ - }, - exclude_none=True, - ) - return _dict + """Return the dictionary representation of the model using alias""" + return self.model_dump(by_alias=True, exclude_unset=True) @classmethod def from_dict(cls, obj: Dict) -> Self: @@ -84,11 +69,6 @@ def from_dict(cls, obj: Dict) -> Self: if not isinstance(obj, dict): return cls.model_validate(obj) - _obj = cls.model_validate({ - "code": obj.get("code"), - "type": obj.get("type"), - "message": obj.get("message") - }) - return _obj + return cls.parse_obj(obj) diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/category.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/category.py index 48b689cba886..cab85b6fdec6 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/category.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/category.py @@ -59,8 +59,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return self.model_dump_json(by_alias=True, exclude_unset=True) @classmethod def from_json(cls, json_str: str) -> Self: @@ -68,22 +67,8 @@ def from_json(cls, json_str: str) -> Self: return cls.from_dict(json.loads(json_str)) def to_dict(self) -> Dict[str, Any]: - """Return the dictionary representation of the model using alias. - - This has the following differences from calling pydantic's - `self.model_dump(by_alias=True)`: - - * `None` is only added to the output dict for nullable fields that - were set at model initialization. Other fields with value `None` - are ignored. - """ - _dict = self.model_dump( - by_alias=True, - exclude={ - }, - exclude_none=True, - ) - return _dict + """Return the dictionary representation of the model using alias""" + return self.model_dump(by_alias=True, exclude_unset=True) @classmethod def from_dict(cls, obj: Dict) -> Self: @@ -94,10 +79,6 @@ def from_dict(cls, obj: Dict) -> Self: if not isinstance(obj, dict): return cls.model_validate(obj) - _obj = cls.model_validate({ - "id": obj.get("id"), - "name": obj.get("name") - }) - return _obj + return cls.parse_obj(obj) diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/order.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/order.py index 7a5a38cdb7b4..b124ff7fa95f 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/order.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/order.py @@ -63,8 +63,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return self.model_dump_json(by_alias=True, exclude_unset=True) @classmethod def from_json(cls, json_str: str) -> Self: @@ -72,22 +71,8 @@ def from_json(cls, json_str: str) -> Self: return cls.from_dict(json.loads(json_str)) def to_dict(self) -> Dict[str, Any]: - """Return the dictionary representation of the model using alias. - - This has the following differences from calling pydantic's - `self.model_dump(by_alias=True)`: - - * `None` is only added to the output dict for nullable fields that - were set at model initialization. Other fields with value `None` - are ignored. - """ - _dict = self.model_dump( - by_alias=True, - exclude={ - }, - exclude_none=True, - ) - return _dict + """Return the dictionary representation of the model using alias""" + return self.model_dump(by_alias=True, exclude_unset=True) @classmethod def from_dict(cls, obj: Dict) -> Self: @@ -98,14 +83,6 @@ def from_dict(cls, obj: Dict) -> Self: if not isinstance(obj, dict): return cls.model_validate(obj) - _obj = cls.model_validate({ - "id": obj.get("id"), - "petId": obj.get("petId"), - "quantity": obj.get("quantity"), - "shipDate": obj.get("shipDate"), - "status": obj.get("status"), - "complete": obj.get("complete") if obj.get("complete") is not None else False - }) - return _obj + return cls.parse_obj(obj) diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/pet.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/pet.py index 450c1b71393f..d2177c4288a3 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/pet.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/pet.py @@ -64,8 +64,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return self.model_dump_json(by_alias=True, exclude_unset=True) @classmethod def from_json(cls, json_str: str) -> Self: @@ -73,32 +72,8 @@ def from_json(cls, json_str: str) -> Self: return cls.from_dict(json.loads(json_str)) def to_dict(self) -> Dict[str, Any]: - """Return the dictionary representation of the model using alias. - - This has the following differences from calling pydantic's - `self.model_dump(by_alias=True)`: - - * `None` is only added to the output dict for nullable fields that - were set at model initialization. Other fields with value `None` - are ignored. - """ - _dict = self.model_dump( - by_alias=True, - exclude={ - }, - exclude_none=True, - ) - # override the default output from pydantic by calling `to_dict()` of category - if self.category: - _dict['category'] = self.category.to_dict() - # override the default output from pydantic by calling `to_dict()` of each item in tags (list) - _items = [] - if self.tags: - for _item in self.tags: - if _item: - _items.append(_item.to_dict()) - _dict['tags'] = _items - return _dict + """Return the dictionary representation of the model using alias""" + return self.model_dump(by_alias=True, exclude_unset=True) @classmethod def from_dict(cls, obj: Dict) -> Self: @@ -109,14 +84,6 @@ def from_dict(cls, obj: Dict) -> Self: if not isinstance(obj, dict): return cls.model_validate(obj) - _obj = cls.model_validate({ - "id": obj.get("id"), - "category": Category.from_dict(obj.get("category")) if obj.get("category") is not None else None, - "name": obj.get("name"), - "photoUrls": obj.get("photoUrls"), - "tags": [Tag.from_dict(_item) for _item in obj.get("tags")] if obj.get("tags") is not None else None, - "status": obj.get("status") - }) - return _obj + return cls.parse_obj(obj) diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/tag.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/tag.py index 8b21d362f55c..31b40d3dcf48 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/tag.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/tag.py @@ -48,8 +48,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return self.model_dump_json(by_alias=True, exclude_unset=True) @classmethod def from_json(cls, json_str: str) -> Self: @@ -57,22 +56,8 @@ def from_json(cls, json_str: str) -> Self: return cls.from_dict(json.loads(json_str)) def to_dict(self) -> Dict[str, Any]: - """Return the dictionary representation of the model using alias. - - This has the following differences from calling pydantic's - `self.model_dump(by_alias=True)`: - - * `None` is only added to the output dict for nullable fields that - were set at model initialization. Other fields with value `None` - are ignored. - """ - _dict = self.model_dump( - by_alias=True, - exclude={ - }, - exclude_none=True, - ) - return _dict + """Return the dictionary representation of the model using alias""" + return self.model_dump(by_alias=True, exclude_unset=True) @classmethod def from_dict(cls, obj: Dict) -> Self: @@ -83,10 +68,6 @@ def from_dict(cls, obj: Dict) -> Self: if not isinstance(obj, dict): return cls.model_validate(obj) - _obj = cls.model_validate({ - "id": obj.get("id"), - "name": obj.get("name") - }) - return _obj + return cls.parse_obj(obj) diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/user.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/user.py index cb98a57479b5..b0e8d044c641 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/user.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/user.py @@ -54,8 +54,7 @@ def to_str(self) -> str: def to_json(self) -> str: """Returns the JSON representation of the model using alias""" - # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead - return json.dumps(self.to_dict()) + return self.model_dump_json(by_alias=True, exclude_unset=True) @classmethod def from_json(cls, json_str: str) -> Self: @@ -63,22 +62,8 @@ def from_json(cls, json_str: str) -> Self: return cls.from_dict(json.loads(json_str)) def to_dict(self) -> Dict[str, Any]: - """Return the dictionary representation of the model using alias. - - This has the following differences from calling pydantic's - `self.model_dump(by_alias=True)`: - - * `None` is only added to the output dict for nullable fields that - were set at model initialization. Other fields with value `None` - are ignored. - """ - _dict = self.model_dump( - by_alias=True, - exclude={ - }, - exclude_none=True, - ) - return _dict + """Return the dictionary representation of the model using alias""" + return self.model_dump(by_alias=True, exclude_unset=True) @classmethod def from_dict(cls, obj: Dict) -> Self: @@ -89,16 +74,6 @@ def from_dict(cls, obj: Dict) -> Self: if not isinstance(obj, dict): return cls.model_validate(obj) - _obj = cls.model_validate({ - "id": obj.get("id"), - "username": obj.get("username"), - "firstName": obj.get("firstName"), - "lastName": obj.get("lastName"), - "email": obj.get("email"), - "password": obj.get("password"), - "phone": obj.get("phone"), - "userStatus": obj.get("userStatus") - }) - return _obj + return cls.parse_obj(obj) From f57a1d50df8839e61b17b7cb681d40aeb1790818 Mon Sep 17 00:00:00 2001 From: Max Goltzsche Date: Tue, 15 Oct 2024 23:03:03 +0200 Subject: [PATCH 3/4] [python-fastapi] exclude unset response fields Exclude unset fields when marshalling api endpoint response bodies. --- .../src/main/resources/python-fastapi/api.mustache | 1 + .../python-fastapi/src/openapi_server/apis/fake_api.py | 1 + .../python-fastapi/src/openapi_server/apis/pet_api.py | 8 ++++++++ .../python-fastapi/src/openapi_server/apis/store_api.py | 4 ++++ .../python-fastapi/src/openapi_server/apis/user_api.py | 8 ++++++++ 5 files changed, 22 insertions(+) diff --git a/modules/openapi-generator/src/main/resources/python-fastapi/api.mustache b/modules/openapi-generator/src/main/resources/python-fastapi/api.mustache index 0680d357cda6..ce7fafa922b7 100644 --- a/modules/openapi-generator/src/main/resources/python-fastapi/api.mustache +++ b/modules/openapi-generator/src/main/resources/python-fastapi/api.mustache @@ -52,6 +52,7 @@ for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."): description = "{{.}}", {{/description}} response_model_by_alias=True, + response_model_exclude_unset=True, ) async def {{operationId}}( {{#allParams}} diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api.py index 0e3f8d51b319..19f5dae78889 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/fake_api.py @@ -44,6 +44,7 @@ tags=["fake"], summary="test query parameter default value", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def fake_query_param_default( has_default: Annotated[Optional[StrictStr], Field(description="has default value")] = Query('Hello World', description="has default value", alias="hasDefault"), diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py index a4aa3a6e71ec..d127926dc499 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/pet_api.py @@ -46,6 +46,7 @@ tags=["pet"], summary="Add a new pet to the store", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def add_pet( pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")] = Body(None, description="Pet object that needs to be added to the store"), @@ -67,6 +68,7 @@ async def add_pet( tags=["pet"], summary="Deletes a pet", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def delete_pet( petId: Annotated[StrictInt, Field(description="Pet id to delete")] = Path(..., description="Pet id to delete"), @@ -90,6 +92,7 @@ async def delete_pet( tags=["pet"], summary="Finds Pets by status", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def find_pets_by_status( status: Annotated[List[StrictStr], Field(description="Status values that need to be considered for filter")] = Query(None, description="Status values that need to be considered for filter", alias="status"), @@ -112,6 +115,7 @@ async def find_pets_by_status( tags=["pet"], summary="Finds Pets by tags", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def find_pets_by_tags( tags: Annotated[List[StrictStr], Field(description="Tags to filter by")] = Query(None, description="Tags to filter by", alias="tags"), @@ -135,6 +139,7 @@ async def find_pets_by_tags( tags=["pet"], summary="Find pet by ID", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def get_pet_by_id( petId: Annotated[StrictInt, Field(description="ID of pet to return")] = Path(..., description="ID of pet to return"), @@ -159,6 +164,7 @@ async def get_pet_by_id( tags=["pet"], summary="Update an existing pet", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def update_pet( pet: Annotated[Pet, Field(description="Pet object that needs to be added to the store")] = Body(None, description="Pet object that needs to be added to the store"), @@ -180,6 +186,7 @@ async def update_pet( tags=["pet"], summary="Updates a pet in the store with form data", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def update_pet_with_form( petId: Annotated[StrictInt, Field(description="ID of pet that needs to be updated")] = Path(..., description="ID of pet that needs to be updated"), @@ -203,6 +210,7 @@ async def update_pet_with_form( tags=["pet"], summary="uploads an image", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def upload_file( petId: Annotated[StrictInt, Field(description="ID of pet to update")] = Path(..., description="ID of pet to update"), diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py index 21d2aceb380d..4d4d56507879 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/store_api.py @@ -45,6 +45,7 @@ tags=["store"], summary="Delete purchase order by ID", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def delete_order( orderId: Annotated[StrictStr, Field(description="ID of the order that needs to be deleted")] = Path(..., description="ID of the order that needs to be deleted"), @@ -63,6 +64,7 @@ async def delete_order( tags=["store"], summary="Returns pet inventories by status", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def get_inventory( token_api_key: TokenModel = Security( @@ -85,6 +87,7 @@ async def get_inventory( tags=["store"], summary="Find purchase order by ID", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def get_order_by_id( orderId: Annotated[int, Field(le=5, strict=True, ge=1, description="ID of pet that needs to be fetched")] = Path(..., description="ID of pet that needs to be fetched", ge=1, le=5), @@ -104,6 +107,7 @@ async def get_order_by_id( tags=["store"], summary="Place an order for a pet", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def place_order( order: Annotated[Order, Field(description="order placed for purchasing the pet")] = Body(None, description="order placed for purchasing the pet"), diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py b/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py index efad9b7d18f3..f5bcca1049ad 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/apis/user_api.py @@ -44,6 +44,7 @@ tags=["user"], summary="Create user", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def create_user( user: Annotated[User, Field(description="Created user object")] = Body(None, description="Created user object"), @@ -65,6 +66,7 @@ async def create_user( tags=["user"], summary="Creates list of users with given input array", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def create_users_with_array_input( user: Annotated[List[User], Field(description="List of user object")] = Body(None, description="List of user object"), @@ -86,6 +88,7 @@ async def create_users_with_array_input( tags=["user"], summary="Creates list of users with given input array", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def create_users_with_list_input( user: Annotated[List[User], Field(description="List of user object")] = Body(None, description="List of user object"), @@ -108,6 +111,7 @@ async def create_users_with_list_input( tags=["user"], summary="Delete user", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def delete_user( username: Annotated[StrictStr, Field(description="The name that needs to be deleted")] = Path(..., description="The name that needs to be deleted"), @@ -131,6 +135,7 @@ async def delete_user( tags=["user"], summary="Get user by user name", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def get_user_by_name( username: Annotated[StrictStr, Field(description="The name that needs to be fetched. Use user1 for testing.")] = Path(..., description="The name that needs to be fetched. Use user1 for testing."), @@ -150,6 +155,7 @@ async def get_user_by_name( tags=["user"], summary="Logs user into the system", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def login_user( username: Annotated[str, Field(strict=True, description="The user name for login")] = Query(None, description="The user name for login", alias="username", regex=r"/^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$/"), @@ -169,6 +175,7 @@ async def login_user( tags=["user"], summary="Logs out current logged in user session", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def logout_user( token_api_key: TokenModel = Security( @@ -190,6 +197,7 @@ async def logout_user( tags=["user"], summary="Updated user", response_model_by_alias=True, + response_model_exclude_unset=True, ) async def update_user( username: Annotated[StrictStr, Field(description="name that need to be deleted")] = Path(..., description="name that need to be deleted"), From ef7c699cf7ecfa17c883f6b801792da5b8f3c41d Mon Sep 17 00:00:00 2001 From: Max Goltzsche Date: Thu, 17 Oct 2024 00:39:13 +0200 Subject: [PATCH 4/4] [python-fastapi] support oneOf the pydantic v2 way * Support oneOf and anyOf schemas the pydantic v2 way by generating them as Unions. * Generate model constructor that forcefully sets the discriminator field to ensure it is included in the marshalled representation. --- .../languages/AbstractPythonCodegen.java | 59 +++++- .../python-fastapi/model_anyof.mustache | 174 +++------------- .../python-fastapi/model_generic.mustache | 7 + .../python-fastapi/model_oneof.mustache | 194 +++--------------- .../python/PythonFastapiCodegenTest.java | 80 +++++++- .../petstore/python-aiohttp/docs/BasquePig.md | 2 +- .../petstore/python-aiohttp/docs/DanishPig.md | 2 +- .../petstore_api/models/basque_pig.py | 4 +- .../petstore_api/models/danish_pig.py | 4 +- .../tests/test_deserialization.py | 4 +- .../client/petstore/python/docs/BasquePig.md | 2 +- .../client/petstore/python/docs/DanishPig.md | 2 +- .../python/petstore_api/models/basque_pig.py | 4 +- .../python/petstore_api/models/danish_pig.py | 4 +- .../python/tests/test_deserialization.py | 4 +- .../src/openapi_server/models/api_response.py | 2 + .../src/openapi_server/models/category.py | 2 + .../src/openapi_server/models/order.py | 2 + .../src/openapi_server/models/pet.py | 2 + .../src/openapi_server/models/tag.py | 2 + .../src/openapi_server/models/user.py | 2 + 21 files changed, 218 insertions(+), 340 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java index c8c76e394692..1f956c3f5eed 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java @@ -823,6 +823,8 @@ public Map postProcessAllModels(Map objs) codegenModelMap.put(cm.classname, ModelUtils.getModelByName(entry.getKey(), objs)); } + propagateDiscriminatorValuesToProperties(processed); + // create circular import for (String m : codegenModelMap.keySet()) { createImportMapOfSet(m, codegenModelMap); @@ -1018,6 +1020,52 @@ private ModelsMap postProcessModelsMap(ModelsMap objs) { return objs; } + private void propagateDiscriminatorValuesToProperties(Map objMap) { + HashMap modelMap = new HashMap<>(); + for (Map.Entry entry : objMap.entrySet()) { + for (ModelMap m : entry.getValue().getModels()) { + modelMap.put("#/components/schemas/" + entry.getKey(), m.getModel()); + } + } + + for (Map.Entry entry : objMap.entrySet()) { + for (ModelMap m : entry.getValue().getModels()) { + CodegenModel model = m.getModel(); + if (model.discriminator != null && !model.oneOf.isEmpty()) { + // Populate default, implicit discriminator values + for (String typeName : model.oneOf) { + ModelsMap obj = objMap.get(typeName); + if (obj == null) { + continue; + } + for (ModelMap m1 : obj.getModels()) { + for (CodegenProperty p : m1.getModel().vars) { + if (p.baseName.equals(model.discriminator.getPropertyBaseName())) { + p.isDiscriminator = true; + p.discriminatorValue = typeName; + } + } + } + } + // Populate explicit discriminator values from mapping, overwriting default values + if (model.discriminator.getMapping() != null) { + for (Map.Entry discrEntry : model.discriminator.getMapping().entrySet()) { + CodegenModel resolved = modelMap.get(discrEntry.getValue()); + if (resolved != null) { + for (CodegenProperty p : resolved.vars) { + if (p.baseName.equals(model.discriminator.getPropertyBaseName())) { + p.isDiscriminator = true; + p.discriminatorValue = discrEntry.getKey(); + } + } + } + } + } + } + } + } + } + /* * Gets the pydantic type given a Codegen Property @@ -2134,7 +2182,16 @@ private PythonType getType(CodegenProperty cp) { } private String finalizeType(CodegenProperty cp, PythonType pt) { - if (!cp.required || cp.isNullable) { + if (cp.isDiscriminator && cp.discriminatorValue != null) { + moduleImports.add("typing", "Literal"); + PythonType literal = new PythonType("Literal"); + String literalValue = '"'+escapeText(cp.discriminatorValue)+'"'; + PythonType valueType = new PythonType(literalValue); + literal.addTypeParam(valueType); + literal.setDefaultValue(literalValue); + cp.setDefaultValue(literalValue); + pt = literal; + } else if (!cp.required || cp.isNullable) { moduleImports.add("typing", "Optional"); PythonType opt = new PythonType("Optional"); opt.addTypeParam(pt); diff --git a/modules/openapi-generator/src/main/resources/python-fastapi/model_anyof.mustache b/modules/openapi-generator/src/main/resources/python-fastapi/model_anyof.mustache index b145f73ad13b..dc945bdaf1ad 100644 --- a/modules/openapi-generator/src/main/resources/python-fastapi/model_anyof.mustache +++ b/modules/openapi-generator/src/main/resources/python-fastapi/model_anyof.mustache @@ -14,174 +14,56 @@ import re # noqa: F401 {{/vendorExtensions.x-py-model-imports}} from typing import Union, Any, List, TYPE_CHECKING, Optional, Dict from typing_extensions import Literal -from pydantic import StrictStr, Field +from pydantic import StrictStr, Field, RootModel try: from typing import Self except ImportError: from typing_extensions import Self -{{#lambda.uppercase}}{{{classname}}}{{/lambda.uppercase}}_ANY_OF_SCHEMAS = [{{#anyOf}}"{{.}}"{{^-last}}, {{/-last}}{{/anyOf}}] - -class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}): +class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}RootModel{{/parent}}): """ {{{description}}}{{^description}}{{{classname}}}{{/description}} """ -{{#composedSchemas.anyOf}} - # data type: {{{dataType}}} - {{vendorExtensions.x-py-name}}: {{{vendorExtensions.x-py-typing}}} -{{/composedSchemas.anyOf}} - if TYPE_CHECKING: - actual_instance: Optional[Union[{{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}]] = None - else: - actual_instance: Any = None - any_of_schemas: List[str] = Literal[{{#lambda.uppercase}}{{{classname}}}{{/lambda.uppercase}}_ANY_OF_SCHEMAS] + root: Union[{{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}] = None model_config = { "validate_assignment": True, "protected_namespaces": (), } -{{#discriminator}} - - discriminator_value_class_map: Dict[str, str] = { -{{#children}} - '{{^vendorExtensions.x-discriminator-value}}{{name}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}': '{{{classname}}}'{{^-last}},{{/-last}} -{{/children}} - } -{{/discriminator}} - - 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): - {{#isNullable}} - if v is None: - return v - - {{/isNullable}} - instance = {{{classname}}}.model_construct() - error_messages = [] - {{#composedSchemas.anyOf}} - # validate data type: {{{dataType}}} - {{#isContainer}} - try: - instance.{{vendorExtensions.x-py-name}} = v - return v - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isContainer}} - {{^isContainer}} - {{#isPrimitiveType}} - try: - instance.{{vendorExtensions.x-py-name}} = v - return v - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isPrimitiveType}} - {{^isPrimitiveType}} - if not isinstance(v, {{{dataType}}}): - error_messages.append(f"Error! Input type `{type(v)}` is not `{{{dataType}}}`") - else: - return v - - {{/isPrimitiveType}} - {{/isContainer}} - {{/composedSchemas.anyOf}} - if error_messages: - # no match - raise ValueError("No match found when setting the actual_instance in {{{classname}}} with anyOf schemas: {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}. Details: " + ", ".join(error_messages)) - else: - return v - - @classmethod - def from_dict(cls, obj: dict) -> 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() - {{#isNullable}} - if json_str is None: - return instance - - {{/isNullable}} - error_messages = [] - {{#composedSchemas.anyOf}} - {{#isContainer}} - # deserialize data into {{{dataType}}} - try: - # validation - instance.{{vendorExtensions.x-py-name}} = json.loads(json_str) - # assign value to actual_instance - instance.actual_instance = instance.{{vendorExtensions.x-py-name}} - return instance - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isContainer}} - {{^isContainer}} - {{#isPrimitiveType}} - # deserialize data into {{{dataType}}} - try: - # validation - instance.{{vendorExtensions.x-py-name}} = json.loads(json_str) - # assign value to actual_instance - instance.actual_instance = instance.{{vendorExtensions.x-py-name}} - return instance - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isPrimitiveType}} - {{^isPrimitiveType}} - # {{vendorExtensions.x-py-name}}: {{{vendorExtensions.x-py-typing}}} - try: - instance.actual_instance = {{{dataType}}}.from_json(json_str) - return instance - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isPrimitiveType}} - {{/isContainer}} - {{/composedSchemas.anyOf}} - - if error_messages: - # no match - raise ValueError("No match found when deserializing the JSON string into {{{classname}}} with anyOf schemas: {{#anyOf}}{{{.}}}{{^-last}}, {{/-last}}{{/anyOf}}. Details: " + ", ".join(error_messages)) - else: - return instance + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) def to_json(self) -> str: - """Returns the JSON representation of the actual instance""" - if self.actual_instance is None: - return "null" + """Returns the JSON representation of the model using alias""" + return self.model_dump_json(by_alias=True, exclude_unset=True) - to_json = getattr(self.actual_instance, "to_json", None) - if callable(to_json): - return self.actual_instance.to_json() + @classmethod + def from_json(cls, json_str: str) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}: + """Create an instance of {{{classname}}} from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias""" + to_dict = getattr(self.root, "to_dict", None) + if callable(to_dict): + return self.model_dump(by_alias=True, exclude_unset=True) else: - return json.dumps(self.actual_instance) + # primitive type + return self.root - def to_dict(self) -> Dict: - """Returns the dict representation of the actual instance""" - if self.actual_instance is None: - return "null" + @classmethod + def from_dict(cls, obj: Dict) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}: + """Create an instance of {{{classname}}} from a dict""" + if obj is None: + return None - to_json = getattr(self.actual_instance, "to_json", None) - if callable(to_json): - return self.actual_instance.to_dict() - else: - # primitive type - return self.actual_instance + if not isinstance(obj, dict): + return cls.model_validate(obj) - def to_str(self) -> str: - """Returns the string representation of the actual instance""" - return pprint.pformat(self.model_dump()) + return cls.parse_obj(obj) {{#vendorExtensions.x-py-postponed-model-imports.size}} {{#vendorExtensions.x-py-postponed-model-imports}} diff --git a/modules/openapi-generator/src/main/resources/python-fastapi/model_generic.mustache b/modules/openapi-generator/src/main/resources/python-fastapi/model_generic.mustache index 4637734528c5..e808ed0c2322 100644 --- a/modules/openapi-generator/src/main/resources/python-fastapi/model_generic.mustache +++ b/modules/openapi-generator/src/main/resources/python-fastapi/model_generic.mustache @@ -86,6 +86,13 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} {{/isAdditionalPropertiesTrue}} } + def __init__(self, *a, **kw): + super().__init__(*a, **kw) + {{#vars}} + {{#isDiscriminator}} + self.{{name}} = self.{{name}} + {{/isDiscriminator}} + {{/vars}} def to_str(self) -> str: """Returns the string representation of the model using alias""" diff --git a/modules/openapi-generator/src/main/resources/python-fastapi/model_oneof.mustache b/modules/openapi-generator/src/main/resources/python-fastapi/model_oneof.mustache index b87c42cf2b9b..ed2166a62776 100644 --- a/modules/openapi-generator/src/main/resources/python-fastapi/model_oneof.mustache +++ b/modules/openapi-generator/src/main/resources/python-fastapi/model_oneof.mustache @@ -13,198 +13,52 @@ import re # noqa: F401 {{{.}}} {{/vendorExtensions.x-py-model-imports}} from typing import Union, Any, List, TYPE_CHECKING, Optional, Dict -from typing_extensions import Literal -from pydantic import StrictStr, Field +from typing_extensions import Annotated, Literal +from pydantic import StrictStr, Field, Discriminator, Tag, RootModel try: from typing import Self except ImportError: from typing_extensions import Self -{{#lambda.uppercase}}{{{classname}}}{{/lambda.uppercase}}_ONE_OF_SCHEMAS = [{{#oneOf}}"{{.}}"{{^-last}}, {{/-last}}{{/oneOf}}] - -class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}): +class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}RootModel{{/parent}}): """ {{{description}}}{{^description}}{{{classname}}}{{/description}} """ -{{#composedSchemas.oneOf}} - # data type: {{{dataType}}} - {{vendorExtensions.x-py-name}}: {{{vendorExtensions.x-py-typing}}} -{{/composedSchemas.oneOf}} - actual_instance: Optional[Union[{{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}]] = None - one_of_schemas: List[str] = Literal[{{#oneOf}}"{{.}}"{{^-last}}, {{/-last}}{{/oneOf}}] + + root: Union[{{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}] = Field({{#discriminator}}{{#mappedModels}}{{#-first}}discriminator='{{{propertyName}}}'{{/-first}}{{/mappedModels}}{{/discriminator}}) model_config = { "validate_assignment": True, "protected_namespaces": (), } -{{#discriminator}} - - discriminator_value_class_map: Dict[str, str] = { -{{#children}} - '{{^vendorExtensions.x-discriminator-value}}{{name}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}': '{{{classname}}}'{{^-last}},{{/-last}} -{{/children}} - } -{{/discriminator}} - - 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_oneof(cls, v): - {{#isNullable}} - if v is None: - return v - - {{/isNullable}} - instance = {{{classname}}}.model_construct() - error_messages = [] - match = 0 - {{#composedSchemas.oneOf}} - # validate data type: {{{dataType}}} - {{#isContainer}} - try: - instance.{{vendorExtensions.x-py-name}} = v - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isContainer}} - {{^isContainer}} - {{#isPrimitiveType}} - try: - instance.{{vendorExtensions.x-py-name}} = v - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isPrimitiveType}} - {{^isPrimitiveType}} - if not isinstance(v, {{{dataType}}}): - error_messages.append(f"Error! Input type `{type(v)}` is not `{{{dataType}}}`") - else: - match += 1 - {{/isPrimitiveType}} - {{/isContainer}} - {{/composedSchemas.oneOf}} - if match > 1: - # more than 1 match - raise ValueError("Multiple matches found when setting `actual_instance` in {{{classname}}} with oneOf schemas: {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}. Details: " + ", ".join(error_messages)) - elif match == 0: - # no match - raise ValueError("No match found when setting `actual_instance` in {{{classname}}} with oneOf schemas: {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}. Details: " + ", ".join(error_messages)) - else: - return v + def to_str(self) -> str: + """Returns the string representation of the model using alias""" + return pprint.pformat(self.model_dump(by_alias=True)) - @classmethod - def from_dict(cls, obj: dict) -> Self: - return cls.from_json(json.dumps(obj)) + def to_json(self) -> str: + """Returns the JSON representation of the model using alias""" + return self.model_dump_json(by_alias=True, exclude_unset=True) @classmethod - def from_json(cls, json_str: str) -> Self: - """Returns the object represented by the json string""" - instance = cls.model_construct() - {{#isNullable}} - if json_str is None: - return instance + def from_json(cls, json_str: str) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}: + """Create an instance of {{{classname}}} from a JSON string""" + return cls.from_dict(json.loads(json_str)) - {{/isNullable}} - error_messages = [] - match = 0 + def to_dict(self) -> Dict[str, Any]: + """Return the dictionary representation of the model using alias""" + return self.model_dump(by_alias=True, exclude_unset=True) - {{#useOneOfDiscriminatorLookup}} - {{#discriminator}} - {{#mappedModels}} - {{#-first}} - # use oneOf discriminator to lookup the data type - _data_type = json.loads(json_str).get("{{{propertyBaseName}}}") - if not _data_type: - raise ValueError("Failed to lookup data type from the field `{{{propertyBaseName}}}` in the input.") - - {{/-first}} - # check if data type is `{{{modelName}}}` - if _data_type == "{{{mappingName}}}": - instance.actual_instance = {{{modelName}}}.from_json(json_str) - return instance - - {{/mappedModels}} - {{/discriminator}} - {{/useOneOfDiscriminatorLookup}} - {{#composedSchemas.oneOf}} - {{#isContainer}} - # deserialize data into {{{dataType}}} - try: - # validation - instance.{{vendorExtensions.x-py-name}} = json.loads(json_str) - # assign value to actual_instance - instance.actual_instance = instance.{{vendorExtensions.x-py-name}} - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isContainer}} - {{^isContainer}} - {{#isPrimitiveType}} - # deserialize data into {{{dataType}}} - try: - # validation - instance.{{vendorExtensions.x-py-name}} = json.loads(json_str) - # assign value to actual_instance - instance.actual_instance = instance.{{vendorExtensions.x-py-name}} - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isPrimitiveType}} - {{^isPrimitiveType}} - # deserialize data into {{{dataType}}} - try: - instance.actual_instance = {{{dataType}}}.from_json(json_str) - match += 1 - except (ValidationError, ValueError) as e: - error_messages.append(str(e)) - {{/isPrimitiveType}} - {{/isContainer}} - {{/composedSchemas.oneOf}} - - if match > 1: - # more than 1 match - raise ValueError("Multiple matches found when deserializing the JSON string into {{{classname}}} with oneOf schemas: {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}. Details: " + ", ".join(error_messages)) - elif match == 0: - # no match - raise ValueError("No match found when deserializing the JSON string into {{{classname}}} with oneOf schemas: {{#oneOf}}{{{.}}}{{^-last}}, {{/-last}}{{/oneOf}}. 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" - - to_json = getattr(self.actual_instance, "to_json", None) - if callable(to_json): - return self.actual_instance.to_json() - else: - return json.dumps(self.actual_instance) - - def to_dict(self) -> Dict: - """Returns the dict representation of the actual instance""" - if self.actual_instance is None: + @classmethod + def from_dict(cls, obj: Dict) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}: + """Create an instance of {{{classname}}} from a dict""" + if obj is None: return None - to_dict = getattr(self.actual_instance, "to_dict", None) - if callable(to_dict): - return self.actual_instance.to_dict() - else: - # primitive type - return self.actual_instance + if not isinstance(obj, dict): + return cls.model_validate(obj) - def to_str(self) -> str: - """Returns the string representation of the actual instance""" - return pprint.pformat(self.model_dump()) + return cls.parse_obj(obj) {{#vendorExtensions.x-py-postponed-model-imports.size}} {{#vendorExtensions.x-py-postponed-model-imports}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java index c4d880e71d37..187d1aa8b7a3 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/python/PythonFastapiCodegenTest.java @@ -1,8 +1,11 @@ package org.openapitools.codegen.python; import com.google.common.collect.Sets; +import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.ComposedSchema; +import io.swagger.v3.oas.models.media.Discriminator; import io.swagger.v3.oas.models.media.Schema; import org.openapitools.codegen.CodegenConstants; import org.openapitools.codegen.CodegenModel; @@ -10,7 +13,9 @@ import org.openapitools.codegen.DefaultGenerator; import org.openapitools.codegen.TestUtils; import org.openapitools.codegen.config.CodegenConfigurator; -import org.openapitools.codegen.languages.PythonClientCodegen; +import org.openapitools.codegen.languages.PythonFastAPIServerCodegen; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.ModelsMap; import org.testng.Assert; import org.testng.annotations.Test; @@ -19,6 +24,8 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; +import java.util.Set; +import java.util.TreeMap; public class PythonFastapiCodegenTest { @Test @@ -63,22 +70,81 @@ public void testEndpointSpecsWithoutDescription() throws IOException { } @Test(description = "additionalProperties should not let container type inherit their type") - public void additionalPropertiesModelTest() { - final Schema model = new ArraySchema() - //.description() + public void testAdditionalProperties() { + Schema model = new ArraySchema() .items(new Schema().type("object").additionalProperties(new Schema().type("string"))) .description("model with additionalProperties"); - final DefaultCodegen codegen = new PythonClientCodegen(); + DefaultCodegen codegen = new PythonFastAPIServerCodegen(); OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", model); codegen.setOpenAPI(openAPI); - final CodegenModel cm = codegen.fromModel("sample", model); + CodegenModel cm = codegen.fromModel("sample", model); Assert.assertEquals(cm.name, "sample"); Assert.assertEquals(cm.classname, "Sample"); Assert.assertEquals(cm.description, "model with additionalProperties"); Assert.assertEquals(cm.vars.size(), 0); - Assert.assertEquals(cm.parent, "null"); + Assert.assertNull(cm.parent, null); Assert.assertEquals(cm.imports.size(), 0); Assert.assertEquals(Sets.intersection(cm.imports, Sets.newHashSet()).size(), 0); } + + @Test(description = "oneOf discriminator mapping values are propagated to vars") + public void testOneOfDiscriminator() { + TreeMap properties1 = new TreeMap<>(); + properties1.put("objectType", new Schema().type("string")); + TreeMap properties2 = new TreeMap<>(properties1); + properties1.put("someProp", new Schema().type("string")); + Schema typeA = new Schema().type("object").properties(properties1); + Schema typeB = new Schema().type("object").properties(properties2); + Schema typeC = new ComposedSchema().oneOf(List.of(typeA, typeB)) + .discriminator(new Discriminator() + .propertyName("objectType") + .mapping("type-a", "#/components/schemas/TypeA")); + Schema typeD = new Schema().type("object").properties(properties2); + + OpenAPI openAPI = TestUtils.createOpenAPI(); + openAPI.setComponents(new Components()); + openAPI.getComponents().addSchemas("TypeA", typeA); + openAPI.getComponents().addSchemas("TypeB", typeB); + openAPI.getComponents().addSchemas("TypeC", typeC); + openAPI.getComponents().addSchemas("TypeD", typeD); + + DefaultCodegen codegen = new PythonFastAPIServerCodegen(); + codegen.setOpenAPI(openAPI); + + TreeMap allModels = new TreeMap<>(); + String[] typeNames = new String[]{"TypeA", "TypeB", "TypeC", "TypeD"}; + CodegenModel[] models = new CodegenModel[]{null, null, null, null}; + for (int i = 0; i < typeNames.length; i++) { + String key = typeNames[i]; + CodegenModel cm = codegen.fromModel(key, openAPI.getComponents().getSchemas().get(key)); + if (key.equals("TypeC")) { + cm.oneOf = Set.of("TypeA", "TypeB"); + } + ModelMap mo = new ModelMap(); + mo.setModel(cm); + ModelsMap objs = new ModelsMap(); + objs.setModels(List.of(mo)); + allModels.put(key, objs); + models[i] = cm; + } + + codegen.postProcessAllModels(allModels); + + CodegenModel typeAModel = models[0]; + CodegenModel typeBModel = models[1]; + CodegenModel typeDModel = models[3]; + Assert.assertEquals(typeAModel.vars.size(), 2); + Assert.assertTrue(typeAModel.vars.get(0).isDiscriminator); + Assert.assertEquals(typeAModel.vars.get(0).discriminatorValue, "type-a"); // explicitly mapped value + Assert.assertTrue(typeBModel.vars.get(0).isDiscriminator); + Assert.assertEquals(typeBModel.vars.get(0).discriminatorValue, "TypeB"); // implicit value + Assert.assertNull(typeAModel.parent); + Assert.assertEquals(typeAModel.imports.size(), 0); + Assert.assertEquals(Sets.intersection(typeAModel.imports, Sets.newHashSet()).size(), 0); + + Assert.assertEquals(typeDModel.vars.size(), 1); + Assert.assertFalse(typeDModel.vars.get(0).isDiscriminator); + Assert.assertNull(typeDModel.vars.get(0).discriminatorValue); + } } diff --git a/samples/openapi3/client/petstore/python-aiohttp/docs/BasquePig.md b/samples/openapi3/client/petstore/python-aiohttp/docs/BasquePig.md index ee28d628722f..ee2b2551e4f5 100644 --- a/samples/openapi3/client/petstore/python-aiohttp/docs/BasquePig.md +++ b/samples/openapi3/client/petstore/python-aiohttp/docs/BasquePig.md @@ -5,7 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**class_name** | **str** | | +**class_name** | **str** | | [default to "BasquePig"] **color** | **str** | | ## Example diff --git a/samples/openapi3/client/petstore/python-aiohttp/docs/DanishPig.md b/samples/openapi3/client/petstore/python-aiohttp/docs/DanishPig.md index 16941388832a..cd7666b41739 100644 --- a/samples/openapi3/client/petstore/python-aiohttp/docs/DanishPig.md +++ b/samples/openapi3/client/petstore/python-aiohttp/docs/DanishPig.md @@ -5,7 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**class_name** | **str** | | +**class_name** | **str** | | [default to "DanishPig"] **size** | **int** | | ## Example diff --git a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/basque_pig.py b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/basque_pig.py index a1f32a6edcfc..7f557c368d94 100644 --- a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/basque_pig.py +++ b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/basque_pig.py @@ -18,7 +18,7 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Literal from typing import Optional, Set from typing_extensions import Self @@ -26,7 +26,7 @@ class BasquePig(BaseModel): """ BasquePig """ # noqa: E501 - class_name: StrictStr = Field(alias="className") + class_name: Literal["BasquePig"] = Field(default="BasquePig", alias="className") color: StrictStr __properties: ClassVar[List[str]] = ["className", "color"] diff --git a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/danish_pig.py b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/danish_pig.py index 061e16a486a5..70bd0a2b6b49 100644 --- a/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/danish_pig.py +++ b/samples/openapi3/client/petstore/python-aiohttp/petstore_api/models/danish_pig.py @@ -18,7 +18,7 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Literal from typing import Optional, Set from typing_extensions import Self @@ -26,7 +26,7 @@ class DanishPig(BaseModel): """ DanishPig """ # noqa: E501 - class_name: StrictStr = Field(alias="className") + class_name: Literal["DanishPig"] = Field(default="DanishPig", alias="className") size: StrictInt __properties: ClassVar[List[str]] = ["className", "size"] diff --git a/samples/openapi3/client/petstore/python-pydantic-v1/tests/test_deserialization.py b/samples/openapi3/client/petstore/python-pydantic-v1/tests/test_deserialization.py index c5fb68663821..0f9677ebd327 100644 --- a/samples/openapi3/client/petstore/python-pydantic-v1/tests/test_deserialization.py +++ b/samples/openapi3/client/petstore/python-pydantic-v1/tests/test_deserialization.py @@ -246,7 +246,7 @@ def test_deserialize_none(self): def test_deserialize_pig(self): """ deserialize pig (oneOf) """ data = { - "className": "BasqueBig", + "className": "BasquePig", "color": "white" } @@ -254,7 +254,7 @@ def test_deserialize_pig(self): deserialized = self.deserialize(response, "Pig") self.assertTrue(isinstance(deserialized.actual_instance, petstore_api.BasquePig)) - self.assertEqual(deserialized.actual_instance.class_name, "BasqueBig") + self.assertEqual(deserialized.actual_instance.class_name, "BasquePig") self.assertEqual(deserialized.actual_instance.color, "white") def test_deserialize_animal(self): diff --git a/samples/openapi3/client/petstore/python/docs/BasquePig.md b/samples/openapi3/client/petstore/python/docs/BasquePig.md index ee28d628722f..ee2b2551e4f5 100644 --- a/samples/openapi3/client/petstore/python/docs/BasquePig.md +++ b/samples/openapi3/client/petstore/python/docs/BasquePig.md @@ -5,7 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**class_name** | **str** | | +**class_name** | **str** | | [default to "BasquePig"] **color** | **str** | | ## Example diff --git a/samples/openapi3/client/petstore/python/docs/DanishPig.md b/samples/openapi3/client/petstore/python/docs/DanishPig.md index 16941388832a..cd7666b41739 100644 --- a/samples/openapi3/client/petstore/python/docs/DanishPig.md +++ b/samples/openapi3/client/petstore/python/docs/DanishPig.md @@ -5,7 +5,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**class_name** | **str** | | +**class_name** | **str** | | [default to "DanishPig"] **size** | **int** | | ## Example diff --git a/samples/openapi3/client/petstore/python/petstore_api/models/basque_pig.py b/samples/openapi3/client/petstore/python/petstore_api/models/basque_pig.py index 4a5b9e3bcb9d..e5c9dbc4cd6e 100644 --- a/samples/openapi3/client/petstore/python/petstore_api/models/basque_pig.py +++ b/samples/openapi3/client/petstore/python/petstore_api/models/basque_pig.py @@ -18,7 +18,7 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictStr -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Literal from typing import Optional, Set from typing_extensions import Self @@ -26,7 +26,7 @@ class BasquePig(BaseModel): """ BasquePig """ # noqa: E501 - class_name: StrictStr = Field(alias="className") + class_name: Literal["BasquePig"] = Field(default="BasquePig", alias="className") color: StrictStr additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["className", "color"] diff --git a/samples/openapi3/client/petstore/python/petstore_api/models/danish_pig.py b/samples/openapi3/client/petstore/python/petstore_api/models/danish_pig.py index df4a80d33908..f70bde48b0b0 100644 --- a/samples/openapi3/client/petstore/python/petstore_api/models/danish_pig.py +++ b/samples/openapi3/client/petstore/python/petstore_api/models/danish_pig.py @@ -18,7 +18,7 @@ import json from pydantic import BaseModel, ConfigDict, Field, StrictInt, StrictStr -from typing import Any, ClassVar, Dict, List +from typing import Any, ClassVar, Dict, List, Literal from typing import Optional, Set from typing_extensions import Self @@ -26,7 +26,7 @@ class DanishPig(BaseModel): """ DanishPig """ # noqa: E501 - class_name: StrictStr = Field(alias="className") + class_name: Literal["DanishPig"] = Field(default="DanishPig", alias="className") size: StrictInt additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["className", "size"] diff --git a/samples/openapi3/client/petstore/python/tests/test_deserialization.py b/samples/openapi3/client/petstore/python/tests/test_deserialization.py index c8ae1a90c77d..b50c412d88a1 100644 --- a/samples/openapi3/client/petstore/python/tests/test_deserialization.py +++ b/samples/openapi3/client/petstore/python/tests/test_deserialization.py @@ -254,7 +254,7 @@ def test_deserialize_none(self): def test_deserialize_pig(self): """ deserialize pig (oneOf) """ data = { - "className": "BasqueBig", + "className": "BasquePig", "color": "white" } @@ -262,7 +262,7 @@ def test_deserialize_pig(self): deserialized = self.deserialize(response, "Pig", 'application/json') self.assertTrue(isinstance(deserialized.actual_instance, petstore_api.BasquePig)) - self.assertEqual(deserialized.actual_instance.class_name, "BasqueBig") + self.assertEqual(deserialized.actual_instance.class_name, "BasquePig") self.assertEqual(deserialized.actual_instance.color, "white") def test_deserialize_animal(self): diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/api_response.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/api_response.py index 1d6fe59b6050..84bb9716530e 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/api_response.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/api_response.py @@ -42,6 +42,8 @@ class ApiResponse(BaseModel): "protected_namespaces": (), } + def __init__(self, *a, **kw): + super().__init__(*a, **kw) def to_str(self) -> str: """Returns the string representation of the model using alias""" diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/category.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/category.py index cab85b6fdec6..57da11251083 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/category.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/category.py @@ -52,6 +52,8 @@ def name_validate_regular_expression(cls, value): "protected_namespaces": (), } + def __init__(self, *a, **kw): + super().__init__(*a, **kw) def to_str(self) -> str: """Returns the string representation of the model using alias""" diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/order.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/order.py index b124ff7fa95f..efc645ee4aa6 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/order.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/order.py @@ -56,6 +56,8 @@ def status_validate_enum(cls, value): "protected_namespaces": (), } + def __init__(self, *a, **kw): + super().__init__(*a, **kw) def to_str(self) -> str: """Returns the string representation of the model using alias""" diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/pet.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/pet.py index d2177c4288a3..bf771365ca22 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/pet.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/pet.py @@ -57,6 +57,8 @@ def status_validate_enum(cls, value): "protected_namespaces": (), } + def __init__(self, *a, **kw): + super().__init__(*a, **kw) def to_str(self) -> str: """Returns the string representation of the model using alias""" diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/tag.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/tag.py index 31b40d3dcf48..fc4b5a4a7fd4 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/tag.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/tag.py @@ -41,6 +41,8 @@ class Tag(BaseModel): "protected_namespaces": (), } + def __init__(self, *a, **kw): + super().__init__(*a, **kw) def to_str(self) -> str: """Returns the string representation of the model using alias""" diff --git a/samples/server/petstore/python-fastapi/src/openapi_server/models/user.py b/samples/server/petstore/python-fastapi/src/openapi_server/models/user.py index b0e8d044c641..b361946973bd 100644 --- a/samples/server/petstore/python-fastapi/src/openapi_server/models/user.py +++ b/samples/server/petstore/python-fastapi/src/openapi_server/models/user.py @@ -47,6 +47,8 @@ class User(BaseModel): "protected_namespaces": (), } + def __init__(self, *a, **kw): + super().__init__(*a, **kw) def to_str(self) -> str: """Returns the string representation of the model using alias"""