Skip to content

Commit 371a3e5

Browse files
committed
[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.
1 parent 3843b18 commit 371a3e5

File tree

7 files changed

+38
-444
lines changed

7 files changed

+38
-444
lines changed

modules/openapi-generator/src/main/resources/python-fastapi/model_generic.mustache

Lines changed: 8 additions & 269 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,6 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
2424
{{#vars}}
2525
{{name}}: {{{vendorExtensions.x-py-typing}}}
2626
{{/vars}}
27-
{{#isAdditionalPropertiesTrue}}
28-
additional_properties: Dict[str, Any] = {}
29-
{{/isAdditionalPropertiesTrue}}
3027
__properties: ClassVar[List[str]] = [{{#allVars}}"{{baseName}}"{{^-last}}, {{/-last}}{{/allVars}}]
3128
{{#vars}}
3229
{{#vendorExtensions.x-regex}}
@@ -84,296 +81,38 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
8481
"populate_by_name": True,
8582
"validate_assignment": True,
8683
"protected_namespaces": (),
84+
{{#isAdditionalPropertiesTrue}}
85+
"extra": "allow",
86+
{{/isAdditionalPropertiesTrue}}
8787
}
8888

89-
90-
{{#hasChildren}}
91-
{{#discriminator}}
92-
# JSON field name that stores the object type
93-
__discriminator_property_name: ClassVar[List[str]] = '{{discriminator.propertyBaseName}}'
94-
95-
# discriminator mappings
96-
__discriminator_value_class_map: ClassVar[Dict[str, str]] = {
97-
{{#mappedModels}}'{{{mappingName}}}': '{{{modelName}}}'{{^-last}},{{/-last}}{{/mappedModels}}
98-
}
99-
100-
@classmethod
101-
def get_discriminator_value(cls, obj: Dict) -> str:
102-
"""Returns the discriminator value (object type) of the data"""
103-
discriminator_value = obj[cls.__discriminator_property_name]
104-
if discriminator_value:
105-
return cls.__discriminator_value_class_map.get(discriminator_value)
106-
else:
107-
return None
108-
109-
{{/discriminator}}
110-
{{/hasChildren}}
11189
def to_str(self) -> str:
11290
"""Returns the string representation of the model using alias"""
11391
return pprint.pformat(self.model_dump(by_alias=True))
11492

11593
def to_json(self) -> str:
11694
"""Returns the JSON representation of the model using alias"""
117-
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
118-
return json.dumps(self.to_dict())
95+
return self.model_dump_json(by_alias=True)
11996

12097
@classmethod
12198
def from_json(cls, json_str: str) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}:
12299
"""Create an instance of {{{classname}}} from a JSON string"""
123-
return cls.from_dict(json.loads(json_str))
100+
return cls.parse_obj(json.loads(json_str))
124101

125102
def to_dict(self) -> Dict[str, Any]:
126-
"""Return the dictionary representation of the model using alias.
127-
128-
This has the following differences from calling pydantic's
129-
`self.model_dump(by_alias=True)`:
130-
131-
* `None` is only added to the output dict for nullable fields that
132-
were set at model initialization. Other fields with value `None`
133-
are ignored.
134-
{{#vendorExtensions.x-py-readonly}}
135-
* OpenAPI `readOnly` fields are excluded.
136-
{{/vendorExtensions.x-py-readonly}}
137-
{{#isAdditionalPropertiesTrue}}
138-
* Fields in `self.additional_properties` are added to the output dict.
139-
{{/isAdditionalPropertiesTrue}}
140-
"""
141-
_dict = self.model_dump(
142-
by_alias=True,
143-
exclude={
144-
{{#vendorExtensions.x-py-readonly}}
145-
"{{{.}}}",
146-
{{/vendorExtensions.x-py-readonly}}
147-
{{#isAdditionalPropertiesTrue}}
148-
"additional_properties",
149-
{{/isAdditionalPropertiesTrue}}
150-
},
151-
exclude_none=True,
152-
)
153-
{{#allVars}}
154-
{{#isContainer}}
155-
{{#isArray}}
156-
{{#items.isArray}}
157-
{{^items.items.isPrimitiveType}}
158-
# override the default output from pydantic by calling `to_dict()` of each item in {{{name}}} (list of list)
159-
_items = []
160-
if self.{{{name}}}:
161-
for _item in self.{{{name}}}:
162-
if _item:
163-
_items.append(
164-
[_inner_item.to_dict() for _inner_item in _item if _inner_item is not None]
165-
)
166-
_dict['{{{baseName}}}'] = _items
167-
{{/items.items.isPrimitiveType}}
168-
{{/items.isArray}}
169-
{{^items.isArray}}
170-
{{^items.isPrimitiveType}}
171-
{{^items.isEnumOrRef}}
172-
# override the default output from pydantic by calling `to_dict()` of each item in {{{name}}} (list)
173-
_items = []
174-
if self.{{{name}}}:
175-
for _item in self.{{{name}}}:
176-
if _item:
177-
_items.append(_item.to_dict())
178-
_dict['{{{baseName}}}'] = _items
179-
{{/items.isEnumOrRef}}
180-
{{/items.isPrimitiveType}}
181-
{{/items.isArray}}
182-
{{/isArray}}
183-
{{#isMap}}
184-
{{#items.isArray}}
185-
{{^items.items.isPrimitiveType}}
186-
# override the default output from pydantic by calling `to_dict()` of each value in {{{name}}} (dict of array)
187-
_field_dict_of_array = {}
188-
if self.{{{name}}}:
189-
for _key in self.{{{name}}}:
190-
if self.{{{name}}}[_key] is not None:
191-
_field_dict_of_array[_key] = [
192-
_item.to_dict() for _item in self.{{{name}}}[_key]
193-
]
194-
_dict['{{{baseName}}}'] = _field_dict_of_array
195-
{{/items.items.isPrimitiveType}}
196-
{{/items.isArray}}
197-
{{^items.isArray}}
198-
{{^items.isPrimitiveType}}
199-
{{^items.isEnumOrRef}}
200-
# override the default output from pydantic by calling `to_dict()` of each value in {{{name}}} (dict)
201-
_field_dict = {}
202-
if self.{{{name}}}:
203-
for _key in self.{{{name}}}:
204-
if self.{{{name}}}[_key]:
205-
_field_dict[_key] = self.{{{name}}}[_key].to_dict()
206-
_dict['{{{baseName}}}'] = _field_dict
207-
{{/items.isEnumOrRef}}
208-
{{/items.isPrimitiveType}}
209-
{{/items.isArray}}
210-
{{/isMap}}
211-
{{/isContainer}}
212-
{{^isContainer}}
213-
{{^isPrimitiveType}}
214-
{{^isEnumOrRef}}
215-
# override the default output from pydantic by calling `to_dict()` of {{{name}}}
216-
if self.{{{name}}}:
217-
_dict['{{{baseName}}}'] = self.{{{name}}}.to_dict()
218-
{{/isEnumOrRef}}
219-
{{/isPrimitiveType}}
220-
{{/isContainer}}
221-
{{/allVars}}
222-
{{#isAdditionalPropertiesTrue}}
223-
# puts key-value pairs in additional_properties in the top level
224-
if self.additional_properties is not None:
225-
for _key, _value in self.additional_properties.items():
226-
_dict[_key] = _value
227-
228-
{{/isAdditionalPropertiesTrue}}
229-
{{#allVars}}
230-
{{#isNullable}}
231-
# set to None if {{{name}}} (nullable) is None
232-
# and model_fields_set contains the field
233-
if self.{{name}} is None and "{{{name}}}" in self.model_fields_set:
234-
_dict['{{{baseName}}}'] = None
235-
236-
{{/isNullable}}
237-
{{/allVars}}
238-
return _dict
103+
"""Return the dictionary representation of the model using alias"""
104+
return self.model_dump(by_alias=True)
239105

240106
@classmethod
241107
def from_dict(cls, obj: Dict) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}:
242108
"""Create an instance of {{{classname}}} from a dict"""
243-
{{#hasChildren}}
244-
{{#discriminator}}
245-
# look up the object type based on discriminator mapping
246-
object_type = cls.get_discriminator_value(obj)
247-
if object_type:
248-
klass = globals()[object_type]
249-
return klass.from_dict(obj)
250-
else:
251-
raise ValueError("{{{classname}}} failed to lookup discriminator value from " +
252-
json.dumps(obj) + ". Discriminator property name: " + cls.__discriminator_property_name +
253-
", mapping: " + json.dumps(cls.__discriminator_value_class_map))
254-
{{/discriminator}}
255-
{{/hasChildren}}
256-
{{^hasChildren}}
257109
if obj is None:
258110
return None
259111

260112
if not isinstance(obj, dict):
261113
return cls.model_validate(obj)
262114

263-
{{#disallowAdditionalPropertiesIfNotPresent}}
264-
{{^isAdditionalPropertiesTrue}}
265-
# raise errors for additional fields in the input
266-
for _key in obj.keys():
267-
if _key not in cls.__properties:
268-
raise ValueError("Error due to additional fields (not defined in {{classname}}) in the input: " + _key)
269-
270-
{{/isAdditionalPropertiesTrue}}
271-
{{/disallowAdditionalPropertiesIfNotPresent}}
272-
_obj = cls.model_validate({
273-
{{#allVars}}
274-
{{#isContainer}}
275-
{{#isArray}}
276-
{{#items.isArray}}
277-
{{#items.items.isPrimitiveType}}
278-
"{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}}
279-
{{/items.items.isPrimitiveType}}
280-
{{^items.items.isPrimitiveType}}
281-
"{{{baseName}}}": [
282-
[{{{items.items.dataType}}}.from_dict(_inner_item) for _inner_item in _item]
283-
for _item in obj.get("{{{baseName}}}")
284-
] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}}
285-
{{/items.items.isPrimitiveType}}
286-
{{/items.isArray}}
287-
{{^items.isArray}}
288-
{{^items.isPrimitiveType}}
289-
{{#items.isEnumOrRef}}
290-
"{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}}
291-
{{/items.isEnumOrRef}}
292-
{{^items.isEnumOrRef}}
293-
"{{{baseName}}}": [{{{items.dataType}}}.from_dict(_item) for _item in obj.get("{{{baseName}}}")] if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}}
294-
{{/items.isEnumOrRef}}
295-
{{/items.isPrimitiveType}}
296-
{{#items.isPrimitiveType}}
297-
"{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}}
298-
{{/items.isPrimitiveType}}
299-
{{/items.isArray}}
300-
{{/isArray}}
301-
{{#isMap}}
302-
{{^items.isPrimitiveType}}
303-
{{^items.isEnumOrRef}}
304-
{{#items.isContainer}}
305-
{{#items.isMap}}
306-
"{{{baseName}}}": dict(
307-
(_k, dict(
308-
(_ik, {{{items.items.dataType}}}.from_dict(_iv))
309-
for _ik, _iv in _v.items()
310-
)
311-
if _v is not None
312-
else None
313-
)
314-
for _k, _v in obj.get("{{{baseName}}}").items()
315-
)
316-
if obj.get("{{{baseName}}}") is not None
317-
else None{{^-last}},{{/-last}}
318-
{{/items.isMap}}
319-
{{#items.isArray}}
320-
"{{{baseName}}}": dict(
321-
(_k,
322-
[{{{items.items.dataType}}}.from_dict(_item) for _item in _v]
323-
if _v is not None
324-
else None
325-
)
326-
for _k, _v in obj.get("{{{baseName}}}").items()
327-
){{^-last}},{{/-last}}
328-
{{/items.isArray}}
329-
{{/items.isContainer}}
330-
{{^items.isContainer}}
331-
"{{{baseName}}}": dict(
332-
(_k, {{{items.dataType}}}.from_dict(_v))
333-
for _k, _v in obj.get("{{{baseName}}}").items()
334-
)
335-
if obj.get("{{{baseName}}}") is not None
336-
else None{{^-last}},{{/-last}}
337-
{{/items.isContainer}}
338-
{{/items.isEnumOrRef}}
339-
{{#items.isEnumOrRef}}
340-
"{{{baseName}}}": dict((_k, _v) for _k, _v in obj.get("{{{baseName}}}").items()){{^-last}},{{/-last}}
341-
{{/items.isEnumOrRef}}
342-
{{/items.isPrimitiveType}}
343-
{{#items.isPrimitiveType}}
344-
"{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}}
345-
{{/items.isPrimitiveType}}
346-
{{/isMap}}
347-
{{/isContainer}}
348-
{{^isContainer}}
349-
{{^isPrimitiveType}}
350-
{{^isEnumOrRef}}
351-
"{{{baseName}}}": {{{dataType}}}.from_dict(obj.get("{{{baseName}}}")) if obj.get("{{{baseName}}}") is not None else None{{^-last}},{{/-last}}
352-
{{/isEnumOrRef}}
353-
{{#isEnumOrRef}}
354-
"{{{baseName}}}": obj.get("{{{baseName}}}"){{#defaultValue}} if obj.get("{{baseName}}") is not None else {{defaultValue}}{{/defaultValue}}{{^-last}},{{/-last}}
355-
{{/isEnumOrRef}}
356-
{{/isPrimitiveType}}
357-
{{#isPrimitiveType}}
358-
{{#defaultValue}}
359-
"{{{baseName}}}": obj.get("{{{baseName}}}") if obj.get("{{{baseName}}}") is not None else {{{defaultValue}}}{{^-last}},{{/-last}}
360-
{{/defaultValue}}
361-
{{^defaultValue}}
362-
"{{{baseName}}}": obj.get("{{{baseName}}}"){{^-last}},{{/-last}}
363-
{{/defaultValue}}
364-
{{/isPrimitiveType}}
365-
{{/isContainer}}
366-
{{/allVars}}
367-
})
368-
{{#isAdditionalPropertiesTrue}}
369-
# store additional fields in additional_properties
370-
for _key in obj.keys():
371-
if _key not in cls.__properties:
372-
_obj.additional_properties[_key] = obj.get(_key)
373-
374-
{{/isAdditionalPropertiesTrue}}
375-
return _obj
376-
{{/hasChildren}}
115+
return cls.parse_obj(obj)
377116

378117
{{#vendorExtensions.x-py-postponed-model-imports.size}}
379118
{{#vendorExtensions.x-py-postponed-model-imports}}

samples/server/petstore/python-fastapi/src/openapi_server/models/api_response.py

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,38 +42,22 @@ class ApiResponse(BaseModel):
4242
"protected_namespaces": (),
4343
}
4444

45-
4645
def to_str(self) -> str:
4746
"""Returns the string representation of the model using alias"""
4847
return pprint.pformat(self.model_dump(by_alias=True))
4948

5049
def to_json(self) -> str:
5150
"""Returns the JSON representation of the model using alias"""
52-
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
53-
return json.dumps(self.to_dict())
51+
return self.model_dump_json(by_alias=True)
5452

5553
@classmethod
5654
def from_json(cls, json_str: str) -> Self:
5755
"""Create an instance of ApiResponse from a JSON string"""
58-
return cls.from_dict(json.loads(json_str))
56+
return cls.parse_obj(json.loads(json_str))
5957

6058
def to_dict(self) -> Dict[str, Any]:
61-
"""Return the dictionary representation of the model using alias.
62-
63-
This has the following differences from calling pydantic's
64-
`self.model_dump(by_alias=True)`:
65-
66-
* `None` is only added to the output dict for nullable fields that
67-
were set at model initialization. Other fields with value `None`
68-
are ignored.
69-
"""
70-
_dict = self.model_dump(
71-
by_alias=True,
72-
exclude={
73-
},
74-
exclude_none=True,
75-
)
76-
return _dict
59+
"""Return the dictionary representation of the model using alias"""
60+
return self.model_dump(by_alias=True)
7761

7862
@classmethod
7963
def from_dict(cls, obj: Dict) -> Self:
@@ -84,11 +68,6 @@ def from_dict(cls, obj: Dict) -> Self:
8468
if not isinstance(obj, dict):
8569
return cls.model_validate(obj)
8670

87-
_obj = cls.model_validate({
88-
"code": obj.get("code"),
89-
"type": obj.get("type"),
90-
"message": obj.get("message")
91-
})
92-
return _obj
71+
return cls.parse_obj(obj)
9372

9473

0 commit comments

Comments
 (0)