Skip to content

Commit 001894e

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 001894e

File tree

7 files changed

+31
-430
lines changed

7 files changed

+31
-430
lines changed

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

Lines changed: 7 additions & 267 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,39 @@ 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

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}}
11190
def to_str(self) -> str:
11291
"""Returns the string representation of the model using alias"""
11392
return pprint.pformat(self.model_dump(by_alias=True))
11493

11594
def to_json(self) -> str:
11695
"""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())
96+
return self.model_dump_json(by_alias=True, exclude_unset=True)
11997

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

125103
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
104+
"""Return the dictionary representation of the model using alias"""
105+
return self.model_dump(by_alias=True, exclude_unset=True)
239106

240107
@classmethod
241108
def from_dict(cls, obj: Dict) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}:
242109
"""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}}
257110
if obj is None:
258111
return None
259112

260113
if not isinstance(obj, dict):
261114
return cls.model_validate(obj)
262115

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

378118
{{#vendorExtensions.x-py-postponed-model-imports.size}}
379119
{{#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

0 commit comments

Comments
 (0)