Skip to content

Commit d83b593

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 d83b593

7 files changed

Lines changed: 31 additions & 409 deletions

File tree

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

Lines changed: 7 additions & 246 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,6 +81,9 @@ 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

8989

@@ -114,266 +114,27 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
114114

115115
def to_json(self) -> str:
116116
"""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())
117+
return self.model_dump_json(by_alias=True, exclude_unset=True)
119118

120119
@classmethod
121120
def from_json(cls, json_str: str) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}:
122121
"""Create an instance of {{{classname}}} from a JSON string"""
123122
return cls.from_dict(json.loads(json_str))
124123

125124
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
125+
"""Return the dictionary representation of the model using alias"""
126+
return self.model_dump(by_alias=True, exclude_unset=True)
239127

240128
@classmethod
241129
def from_dict(cls, obj: Dict) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}:
242130
"""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}}
257131
if obj is None:
258132
return None
259133

260134
if not isinstance(obj, dict):
261135
return cls.model_validate(obj)
262136

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}}
137+
return cls.parse_obj(obj)
377138

378139
{{#vendorExtensions.x-py-postponed-model-imports.size}}
379140
{{#vendorExtensions.x-py-postponed-model-imports}}

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

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -49,31 +49,16 @@ def to_str(self) -> str:
4949

5050
def to_json(self) -> str:
5151
"""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())
52+
return self.model_dump_json(by_alias=True, exclude_unset=True)
5453

5554
@classmethod
5655
def from_json(cls, json_str: str) -> Self:
5756
"""Create an instance of ApiResponse from a JSON string"""
5857
return cls.from_dict(json.loads(json_str))
5958

6059
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
60+
"""Return the dictionary representation of the model using alias"""
61+
return self.model_dump(by_alias=True, exclude_unset=True)
7762

7863
@classmethod
7964
def from_dict(cls, obj: Dict) -> Self:
@@ -84,11 +69,6 @@ def from_dict(cls, obj: Dict) -> Self:
8469
if not isinstance(obj, dict):
8570
return cls.model_validate(obj)
8671

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

9474

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

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -59,31 +59,16 @@ def to_str(self) -> str:
5959

6060
def to_json(self) -> str:
6161
"""Returns the JSON representation of the model using alias"""
62-
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
63-
return json.dumps(self.to_dict())
62+
return self.model_dump_json(by_alias=True, exclude_unset=True)
6463

6564
@classmethod
6665
def from_json(cls, json_str: str) -> Self:
6766
"""Create an instance of Category from a JSON string"""
6867
return cls.from_dict(json.loads(json_str))
6968

7069
def to_dict(self) -> Dict[str, Any]:
71-
"""Return the dictionary representation of the model using alias.
72-
73-
This has the following differences from calling pydantic's
74-
`self.model_dump(by_alias=True)`:
75-
76-
* `None` is only added to the output dict for nullable fields that
77-
were set at model initialization. Other fields with value `None`
78-
are ignored.
79-
"""
80-
_dict = self.model_dump(
81-
by_alias=True,
82-
exclude={
83-
},
84-
exclude_none=True,
85-
)
86-
return _dict
70+
"""Return the dictionary representation of the model using alias"""
71+
return self.model_dump(by_alias=True, exclude_unset=True)
8772

8873
@classmethod
8974
def from_dict(cls, obj: Dict) -> Self:
@@ -94,10 +79,6 @@ def from_dict(cls, obj: Dict) -> Self:
9479
if not isinstance(obj, dict):
9580
return cls.model_validate(obj)
9681

97-
_obj = cls.model_validate({
98-
"id": obj.get("id"),
99-
"name": obj.get("name")
100-
})
101-
return _obj
82+
return cls.parse_obj(obj)
10283

10384

0 commit comments

Comments
 (0)