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/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/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/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 41354ee62410..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 @@ -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,26 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}} "populate_by_name": True, "validate_assignment": True, "protected_namespaces": (), +{{#isAdditionalPropertiesTrue}} + "extra": "allow", +{{/isAdditionalPropertiesTrue}} } + def __init__(self, *a, **kw): + super().__init__(*a, **kw) + {{#vars}} + {{#isDiscriminator}} + self.{{name}} = self.{{name}} + {{/isDiscriminator}} + {{/vars}} -{{#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 +108,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/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 9a222ac9fae3..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,9 +1,22 @@ 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; +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.PythonFastAPIServerCodegen; +import org.openapitools.codegen.model.ModelMap; +import org.openapitools.codegen.model.ModelsMap; +import org.testng.Assert; import org.testng.annotations.Test; import java.io.File; @@ -11,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 @@ -53,4 +68,83 @@ 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 testAdditionalProperties() { + Schema model = new ArraySchema() + .items(new Schema().type("object").additionalProperties(new Schema().type("string"))) + .description("model with additionalProperties"); + DefaultCodegen codegen = new PythonFastAPIServerCodegen(); + OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", model); + codegen.setOpenAPI(openAPI); + 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.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/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"), 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..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""" @@ -49,8 +51,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 +59,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 +71,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..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""" @@ -59,8 +61,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 +69,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 +81,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..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""" @@ -63,8 +65,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 +73,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 +85,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..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""" @@ -64,8 +66,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 +74,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 +86,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..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""" @@ -48,8 +50,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 +58,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 +70,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..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""" @@ -54,8 +56,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 +64,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 +76,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)