Skip to content

Commit 24b70a9

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. * Don't generate the `{to|from}_{dict|json}` methods since pydantic is taking care of the model mapping based on the declared fields and `model_config` - users can use `model.model_dump[_json]` instead.
1 parent 3843b18 commit 24b70a9

File tree

7 files changed

+3
-570
lines changed

7 files changed

+3
-570
lines changed

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

Lines changed: 3 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,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

@@ -108,272 +108,6 @@ class {{classname}}({{#parent}}{{{.}}}{{/parent}}{{^parent}}BaseModel{{/parent}}
108108

109109
{{/discriminator}}
110110
{{/hasChildren}}
111-
def to_str(self) -> str:
112-
"""Returns the string representation of the model using alias"""
113-
return pprint.pformat(self.model_dump(by_alias=True))
114-
115-
def to_json(self) -> str:
116-
"""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())
119-
120-
@classmethod
121-
def from_json(cls, json_str: str) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}:
122-
"""Create an instance of {{{classname}}} from a JSON string"""
123-
return cls.from_dict(json.loads(json_str))
124-
125-
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
239-
240-
@classmethod
241-
def from_dict(cls, obj: Dict) -> {{^hasChildren}}Self{{/hasChildren}}{{#hasChildren}}{{#discriminator}}Union[{{#children}}Self{{^-last}}, {{/-last}}{{/children}}]{{/discriminator}}{{^discriminator}}Self{{/discriminator}}{{/hasChildren}}:
242-
"""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}}
257-
if obj is None:
258-
return None
259-
260-
if not isinstance(obj, dict):
261-
return cls.model_validate(obj)
262-
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}}
377111

378112
{{#vendorExtensions.x-py-postponed-model-imports.size}}
379113
{{#vendorExtensions.x-py-postponed-model-imports}}

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

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -43,52 +43,5 @@ class ApiResponse(BaseModel):
4343
}
4444

4545

46-
def to_str(self) -> str:
47-
"""Returns the string representation of the model using alias"""
48-
return pprint.pformat(self.model_dump(by_alias=True))
49-
50-
def to_json(self) -> str:
51-
"""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())
54-
55-
@classmethod
56-
def from_json(cls, json_str: str) -> Self:
57-
"""Create an instance of ApiResponse from a JSON string"""
58-
return cls.from_dict(json.loads(json_str))
59-
60-
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
77-
78-
@classmethod
79-
def from_dict(cls, obj: Dict) -> Self:
80-
"""Create an instance of ApiResponse from a dict"""
81-
if obj is None:
82-
return None
83-
84-
if not isinstance(obj, dict):
85-
return cls.model_validate(obj)
86-
87-
_obj = cls.model_validate({
88-
"code": obj.get("code"),
89-
"type": obj.get("type"),
90-
"message": obj.get("message")
91-
})
92-
return _obj
9346

9447

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

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -53,51 +53,5 @@ def name_validate_regular_expression(cls, value):
5353
}
5454

5555

56-
def to_str(self) -> str:
57-
"""Returns the string representation of the model using alias"""
58-
return pprint.pformat(self.model_dump(by_alias=True))
59-
60-
def to_json(self) -> str:
61-
"""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())
64-
65-
@classmethod
66-
def from_json(cls, json_str: str) -> Self:
67-
"""Create an instance of Category from a JSON string"""
68-
return cls.from_dict(json.loads(json_str))
69-
70-
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
87-
88-
@classmethod
89-
def from_dict(cls, obj: Dict) -> Self:
90-
"""Create an instance of Category from a dict"""
91-
if obj is None:
92-
return None
93-
94-
if not isinstance(obj, dict):
95-
return cls.model_validate(obj)
96-
97-
_obj = cls.model_validate({
98-
"id": obj.get("id"),
99-
"name": obj.get("name")
100-
})
101-
return _obj
10256

10357

0 commit comments

Comments
 (0)