Skip to content

Commit 84d9844

Browse files
authored
Fix nullable & required properties in multipart bodies (#995)
WIP Fix for #926 --------- Co-authored-by: Dylan Anthony <[email protected]>
1 parent 9cbca04 commit 84d9844

22 files changed

+303
-101
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
default: patch
3+
---
4+
5+
# Fix nullable and required properties in multipart bodies
6+
7+
Fixes #926.
8+
9+
> [!WARNING]
10+
> This change is likely to break custom templates. Multipart body handling has been completely split from JSON bodies.

end_to_end_tests/baseline_openapi_3.0.json

+30-16
Original file line numberDiff line numberDiff line change
@@ -1415,7 +1415,9 @@
14151415
},
14161416
"/naming/mixed-case": {
14171417
"get": {
1418-
"tags": ["naming"],
1418+
"tags": [
1419+
"naming"
1420+
],
14191421
"operationId": "mixed_case",
14201422
"parameters": [
14211423
{
@@ -1436,30 +1438,32 @@
14361438
}
14371439
],
14381440
"responses": {
1439-
"200": {
1440-
"description": "Successful response",
1441-
"content": {
1442-
"application/json": {
1443-
"schema": {
1444-
"type": "object",
1445-
"properties": {
1446-
"mixed_case": {
1447-
"type": "string"
1448-
},
1449-
"mixedCase": {
1450-
"type": "string"
1451-
}
1441+
"200": {
1442+
"description": "Successful response",
1443+
"content": {
1444+
"application/json": {
1445+
"schema": {
1446+
"type": "object",
1447+
"properties": {
1448+
"mixed_case": {
1449+
"type": "string"
1450+
},
1451+
"mixedCase": {
1452+
"type": "string"
14521453
}
14531454
}
14541455
}
14551456
}
14561457
}
1458+
}
14571459
}
14581460
}
14591461
},
14601462
"/naming/{hyphen-in-path}": {
14611463
"get": {
1462-
"tags": ["naming"],
1464+
"tags": [
1465+
"naming"
1466+
],
14631467
"operationId": "hyphen_in_path",
14641468
"parameters": [
14651469
{
@@ -1863,7 +1867,8 @@
18631867
"required": [
18641868
"some_file",
18651869
"some_object",
1866-
"some_nullable_object"
1870+
"some_nullable_object",
1871+
"some_required_number"
18671872
],
18681873
"type": "object",
18691874
"properties": {
@@ -1896,6 +1901,15 @@
18961901
"title": "Some Number",
18971902
"type": "number"
18981903
},
1904+
"some_nullable_number": {
1905+
"title": "Some Nullable Number",
1906+
"type": "number",
1907+
"nullable": true
1908+
},
1909+
"some_required_number": {
1910+
"title": "Some Required Number",
1911+
"type": "number"
1912+
},
18991913
"some_array": {
19001914
"title": "Some Array",
19011915
"nullable": true,

end_to_end_tests/baseline_openapi_3.1.yaml

+10-1
Original file line numberDiff line numberDiff line change
@@ -1877,7 +1877,8 @@ info:
18771877
"required": [
18781878
"some_file",
18791879
"some_object",
1880-
"some_nullable_object"
1880+
"some_nullable_object",
1881+
"some_required_number"
18811882
],
18821883
"type": "object",
18831884
"properties": {
@@ -1910,6 +1911,14 @@ info:
19101911
"title": "Some Number",
19111912
"type": "number"
19121913
},
1914+
"some_nullable_number": {
1915+
"title": "Some Nullable Number",
1916+
"type": [ "number", "null" ]
1917+
},
1918+
"some_required_number": {
1919+
"title": "Some Number",
1920+
"type": "number"
1921+
},
19131922
"some_array": {
19141923
"title": "Some Array",
19151924
"type": [ "array", "null" ],

end_to_end_tests/golden-record/my_test_api_client/models/body_upload_file_tests_upload_post.py

+48-5
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,30 @@ class BodyUploadFileTestsUploadPost:
3232
"""
3333
Attributes:
3434
some_file (File):
35+
some_required_number (float):
3536
some_object (BodyUploadFileTestsUploadPostSomeObject):
3637
some_nullable_object (Union['BodyUploadFileTestsUploadPostSomeNullableObject', None]):
3738
some_optional_file (Union[Unset, File]):
3839
some_string (Union[Unset, str]): Default: 'some_default_string'.
3940
a_datetime (Union[Unset, datetime.datetime]):
4041
a_date (Union[Unset, datetime.date]):
4142
some_number (Union[Unset, float]):
43+
some_nullable_number (Union[None, Unset, float]):
4244
some_array (Union[List['AFormData'], None, Unset]):
4345
some_optional_object (Union[Unset, BodyUploadFileTestsUploadPostSomeOptionalObject]):
4446
some_enum (Union[Unset, DifferentEnum]): An enumeration.
4547
"""
4648

4749
some_file: File
50+
some_required_number: float
4851
some_object: "BodyUploadFileTestsUploadPostSomeObject"
4952
some_nullable_object: Union["BodyUploadFileTestsUploadPostSomeNullableObject", None]
5053
some_optional_file: Union[Unset, File] = UNSET
5154
some_string: Union[Unset, str] = "some_default_string"
5255
a_datetime: Union[Unset, datetime.datetime] = UNSET
5356
a_date: Union[Unset, datetime.date] = UNSET
5457
some_number: Union[Unset, float] = UNSET
58+
some_nullable_number: Union[None, Unset, float] = UNSET
5559
some_array: Union[List["AFormData"], None, Unset] = UNSET
5660
some_optional_object: Union[Unset, "BodyUploadFileTestsUploadPostSomeOptionalObject"] = UNSET
5761
some_enum: Union[Unset, DifferentEnum] = UNSET
@@ -66,6 +70,8 @@ def to_dict(self) -> Dict[str, Any]:
6670

6771
some_file = self.some_file.to_tuple()
6872

73+
some_required_number = self.some_required_number
74+
6975
some_object = self.some_object.to_dict()
7076

7177
some_nullable_object: Union[Dict[str, Any], None]
@@ -90,6 +96,12 @@ def to_dict(self) -> Dict[str, Any]:
9096

9197
some_number = self.some_number
9298

99+
some_nullable_number: Union[None, Unset, float]
100+
if isinstance(self.some_nullable_number, Unset):
101+
some_nullable_number = UNSET
102+
else:
103+
some_nullable_number = self.some_nullable_number
104+
93105
some_array: Union[List[Dict[str, Any]], None, Unset]
94106
if isinstance(self.some_array, Unset):
95107
some_array = UNSET
@@ -116,6 +128,7 @@ def to_dict(self) -> Dict[str, Any]:
116128
field_dict.update(
117129
{
118130
"some_file": some_file,
131+
"some_required_number": some_required_number,
119132
"some_object": some_object,
120133
"some_nullable_object": some_nullable_object,
121134
}
@@ -130,6 +143,8 @@ def to_dict(self) -> Dict[str, Any]:
130143
field_dict["a_date"] = a_date
131144
if some_number is not UNSET:
132145
field_dict["some_number"] = some_number
146+
if some_nullable_number is not UNSET:
147+
field_dict["some_nullable_number"] = some_nullable_number
133148
if some_array is not UNSET:
134149
field_dict["some_array"] = some_array
135150
if some_optional_object is not UNSET:
@@ -142,13 +157,16 @@ def to_dict(self) -> Dict[str, Any]:
142157
def to_multipart(self) -> Dict[str, Any]:
143158
some_file = self.some_file.to_tuple()
144159

160+
some_required_number = (None, str(self.some_required_number).encode(), "text/plain")
161+
145162
some_object = (None, json.dumps(self.some_object.to_dict()).encode(), "application/json")
146163

147-
some_nullable_object: Union[None, Tuple[None, bytes, str]]
164+
some_nullable_object: Tuple[None, bytes, str]
165+
148166
if isinstance(self.some_nullable_object, BodyUploadFileTestsUploadPostSomeNullableObject):
149167
some_nullable_object = (None, json.dumps(self.some_nullable_object.to_dict()).encode(), "application/json")
150168
else:
151-
some_nullable_object = self.some_nullable_object
169+
some_nullable_object = (None, str(self.some_nullable_object).encode(), "text/plain")
152170

153171
some_optional_file: Union[Unset, FileJsonType] = UNSET
154172
if not isinstance(self.some_optional_file, Unset):
@@ -174,7 +192,17 @@ def to_multipart(self) -> Dict[str, Any]:
174192
else (None, str(self.some_number).encode(), "text/plain")
175193
)
176194

177-
some_array: Union[None, Tuple[None, bytes, str], Unset]
195+
some_nullable_number: Union[Tuple[None, bytes, str], Unset]
196+
197+
if isinstance(self.some_nullable_number, Unset):
198+
some_nullable_number = UNSET
199+
elif isinstance(self.some_nullable_number, float):
200+
some_nullable_number = (None, str(self.some_nullable_number).encode(), "text/plain")
201+
else:
202+
some_nullable_number = (None, str(self.some_nullable_number).encode(), "text/plain")
203+
204+
some_array: Union[Tuple[None, bytes, str], Unset]
205+
178206
if isinstance(self.some_array, Unset):
179207
some_array = UNSET
180208
elif isinstance(self.some_array, list):
@@ -183,9 +211,8 @@ def to_multipart(self) -> Dict[str, Any]:
183211
some_array_type_0_item = some_array_type_0_item_data.to_dict()
184212
_temp_some_array.append(some_array_type_0_item)
185213
some_array = (None, json.dumps(_temp_some_array).encode(), "application/json")
186-
187214
else:
188-
some_array = self.some_array
215+
some_array = (None, str(self.some_array).encode(), "text/plain")
189216

190217
some_optional_object: Union[Unset, Tuple[None, bytes, str]] = UNSET
191218
if not isinstance(self.some_optional_object, Unset):
@@ -201,6 +228,7 @@ def to_multipart(self) -> Dict[str, Any]:
201228
field_dict.update(
202229
{
203230
"some_file": some_file,
231+
"some_required_number": some_required_number,
204232
"some_object": some_object,
205233
"some_nullable_object": some_nullable_object,
206234
}
@@ -215,6 +243,8 @@ def to_multipart(self) -> Dict[str, Any]:
215243
field_dict["a_date"] = a_date
216244
if some_number is not UNSET:
217245
field_dict["some_number"] = some_number
246+
if some_nullable_number is not UNSET:
247+
field_dict["some_nullable_number"] = some_nullable_number
218248
if some_array is not UNSET:
219249
field_dict["some_array"] = some_array
220250
if some_optional_object is not UNSET:
@@ -241,6 +271,8 @@ def from_dict(cls: Type[T], src_dict: Dict[str, Any]) -> T:
241271
d = src_dict.copy()
242272
some_file = File(payload=BytesIO(d.pop("some_file")))
243273

274+
some_required_number = d.pop("some_required_number")
275+
244276
some_object = BodyUploadFileTestsUploadPostSomeObject.from_dict(d.pop("some_object"))
245277

246278
def _parse_some_nullable_object(data: object) -> Union["BodyUploadFileTestsUploadPostSomeNullableObject", None]:
@@ -283,6 +315,15 @@ def _parse_some_nullable_object(data: object) -> Union["BodyUploadFileTestsUploa
283315

284316
some_number = d.pop("some_number", UNSET)
285317

318+
def _parse_some_nullable_number(data: object) -> Union[None, Unset, float]:
319+
if data is None:
320+
return data
321+
if isinstance(data, Unset):
322+
return data
323+
return cast(Union[None, Unset, float], data)
324+
325+
some_nullable_number = _parse_some_nullable_number(d.pop("some_nullable_number", UNSET))
326+
286327
def _parse_some_array(data: object) -> Union[List["AFormData"], None, Unset]:
287328
if data is None:
288329
return data
@@ -321,13 +362,15 @@ def _parse_some_array(data: object) -> Union[List["AFormData"], None, Unset]:
321362

322363
body_upload_file_tests_upload_post = cls(
323364
some_file=some_file,
365+
some_required_number=some_required_number,
324366
some_object=some_object,
325367
some_nullable_object=some_nullable_object,
326368
some_optional_file=some_optional_file,
327369
some_string=some_string,
328370
a_datetime=a_datetime,
329371
a_date=a_date,
330372
some_number=some_number,
373+
some_nullable_number=some_nullable_number,
331374
some_array=some_array,
332375
some_optional_object=some_optional_object,
333376
some_enum=some_enum,

end_to_end_tests/golden-record/my_test_api_client/models/post_bodies_multiple_files_body.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ def to_multipart(self) -> Dict[str, Any]:
3333
a = self.a if isinstance(self.a, Unset) else (None, str(self.a).encode(), "text/plain")
3434

3535
field_dict: Dict[str, Any] = {}
36-
field_dict.update(
37-
{key: (None, str(value).encode(), "text/plain") for key, value in self.additional_properties.items()}
38-
)
36+
for prop_name, prop in self.additional_properties.items():
37+
field_dict[prop_name] = (None, str(prop).encode(), "text/plain")
38+
3939
field_dict.update({})
4040
if a is not UNSET:
4141
field_dict["a"] = a

integration-tests/integration_tests/models/post_body_multipart_body.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ def to_dict(self) -> Dict[str, Any]:
4545
return field_dict
4646

4747
def to_multipart(self) -> Dict[str, Any]:
48-
a_string = (
49-
self.a_string if isinstance(self.a_string, Unset) else (None, str(self.a_string).encode(), "text/plain")
50-
)
48+
a_string = (None, str(self.a_string).encode(), "text/plain")
5149

5250
file = self.file.to_tuple()
5351

@@ -58,9 +56,9 @@ def to_multipart(self) -> Dict[str, Any]:
5856
)
5957

6058
field_dict: Dict[str, Any] = {}
61-
field_dict.update(
62-
{key: (None, str(value).encode(), "text/plain") for key, value in self.additional_properties.items()}
63-
)
59+
for prop_name, prop in self.additional_properties.items():
60+
field_dict[prop_name] = (None, str(prop).encode(), "text/plain")
61+
6462
field_dict.update(
6563
{
6664
"a_string": a_string,

0 commit comments

Comments
 (0)