Skip to content

Commit 30b27c2

Browse files
fix: Initialize optional lists as UNSET, not [] (#1346)
## Summary Optional list properties were being incorrectly initialized with empty lists `[]` instead of `UNSET` when calling `from_dict()` with missing values. This breaks the semantic distinction between an omitted field and a field explicitly set to an empty list, which matters for PATCH operations and APIs that treat these cases differently. ## Changes The fix modifies the `list_property.py.jinja` template to: - Initialize optional list properties as UNSET with proper type annotations - Only create and populate lists when the source value is not UNSET - Preserve existing behavior for required list properties ## Test Plan - All existing tests pass (417 tests) - Updated golden record snapshots to reflect the corrected behavior - Changes are minimal and focused on optional list properties only Fixes #1305 Fixes #961
1 parent a6a636c commit 30b27c2

File tree

12 files changed

+144
-110
lines changed

12 files changed

+144
-110
lines changed

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -395,17 +395,19 @@ def _parse_nullable_model(data: object) -> ModelWithUnionProperty | None:
395395
else:
396396
an_optional_allof_enum = AnAllOfEnum(_an_optional_allof_enum)
397397

398-
nested_list_of_enums = []
399398
_nested_list_of_enums = d.pop("nested_list_of_enums", UNSET)
400-
for nested_list_of_enums_item_data in _nested_list_of_enums or []:
401-
nested_list_of_enums_item = []
402-
_nested_list_of_enums_item = nested_list_of_enums_item_data
403-
for nested_list_of_enums_item_item_data in _nested_list_of_enums_item:
404-
nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data)
399+
nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET
400+
if _nested_list_of_enums is not UNSET:
401+
nested_list_of_enums = []
402+
for nested_list_of_enums_item_data in _nested_list_of_enums:
403+
nested_list_of_enums_item = []
404+
_nested_list_of_enums_item = nested_list_of_enums_item_data
405+
for nested_list_of_enums_item_item_data in _nested_list_of_enums_item:
406+
nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data)
405407

406-
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
408+
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
407409

408-
nested_list_of_enums.append(nested_list_of_enums_item)
410+
nested_list_of_enums.append(nested_list_of_enums_item)
409411

410412
_a_not_required_date = d.pop("a_not_required_date", UNSET)
411413
a_not_required_date: datetime.date | Unset

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,18 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
5050
)
5151

5252
d = dict(src_dict)
53-
circular = []
5453
_circular = d.pop("circular", UNSET)
55-
for componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data in _circular or []:
56-
componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item = (
57-
AnArrayWithACircularRefInItemsObjectBItem.from_dict(
58-
componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data
54+
circular: list[AnArrayWithACircularRefInItemsObjectBItem] | Unset = UNSET
55+
if _circular is not UNSET:
56+
circular = []
57+
for componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data in _circular:
58+
componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item = (
59+
AnArrayWithACircularRefInItemsObjectBItem.from_dict(
60+
componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item_data
61+
)
5962
)
60-
)
6163

62-
circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item)
64+
circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_b_item)
6365

6466
an_array_with_a_circular_ref_in_items_object_a_item = cls(
6567
circular=circular,

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,16 +50,18 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
5050
)
5151

5252
d = dict(src_dict)
53-
circular = []
5453
_circular = d.pop("circular", UNSET)
55-
for componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data in _circular or []:
56-
componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item = (
57-
AnArrayWithACircularRefInItemsObjectAItem.from_dict(
58-
componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data
54+
circular: list[AnArrayWithACircularRefInItemsObjectAItem] | Unset = UNSET
55+
if _circular is not UNSET:
56+
circular = []
57+
for componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data in _circular:
58+
componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item = (
59+
AnArrayWithACircularRefInItemsObjectAItem.from_dict(
60+
componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item_data
61+
)
5962
)
60-
)
6163

62-
circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item)
64+
circular.append(componentsschemas_an_array_with_a_circular_ref_in_items_object_a_item)
6365

6466
an_array_with_a_circular_ref_in_items_object_b_item = cls(
6567
circular=circular,

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,18 @@ def to_dict(self) -> dict[str, Any]:
4242
@classmethod
4343
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
4444
d = dict(src_dict)
45-
recursive = []
4645
_recursive = d.pop("recursive", UNSET)
47-
for componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data in _recursive or []:
48-
componentsschemas_an_array_with_a_recursive_ref_in_items_object_item = (
49-
AnArrayWithARecursiveRefInItemsObjectItem.from_dict(
50-
componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data
46+
recursive: list[AnArrayWithARecursiveRefInItemsObjectItem] | Unset = UNSET
47+
if _recursive is not UNSET:
48+
recursive = []
49+
for componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data in _recursive:
50+
componentsschemas_an_array_with_a_recursive_ref_in_items_object_item = (
51+
AnArrayWithARecursiveRefInItemsObjectItem.from_dict(
52+
componentsschemas_an_array_with_a_recursive_ref_in_items_object_item_data
53+
)
5154
)
52-
)
5355

54-
recursive.append(componentsschemas_an_array_with_a_recursive_ref_in_items_object_item)
56+
recursive.append(componentsschemas_an_array_with_a_recursive_ref_in_items_object_item)
5557

5658
an_array_with_a_recursive_ref_in_items_object_item = cls(
5759
recursive=recursive,

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -316,18 +316,20 @@ def _parse_some_nullable_number(data: object) -> float | None | Unset:
316316

317317
some_nullable_number = _parse_some_nullable_number(d.pop("some_nullable_number", UNSET))
318318

319-
some_int_array = []
320319
_some_int_array = d.pop("some_int_array", UNSET)
321-
for some_int_array_item_data in _some_int_array or []:
320+
some_int_array: list[int | None] | Unset = UNSET
321+
if _some_int_array is not UNSET:
322+
some_int_array = []
323+
for some_int_array_item_data in _some_int_array:
322324

323-
def _parse_some_int_array_item(data: object) -> int | None:
324-
if data is None:
325-
return data
326-
return cast(int | None, data)
325+
def _parse_some_int_array_item(data: object) -> int | None:
326+
if data is None:
327+
return data
328+
return cast(int | None, data)
327329

328-
some_int_array_item = _parse_some_int_array_item(some_int_array_item_data)
330+
some_int_array_item = _parse_some_int_array_item(some_int_array_item_data)
329331

330-
some_int_array.append(some_int_array_item)
332+
some_int_array.append(some_int_array_item)
331333

332334
def _parse_some_array(data: object) -> list[AFormData] | None | Unset:
333335
if data is None:

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -402,17 +402,19 @@ def _parse_nullable_model(data: object) -> ModelWithUnionProperty | None:
402402
else:
403403
an_optional_allof_enum = AnAllOfEnum(_an_optional_allof_enum)
404404

405-
nested_list_of_enums = []
406405
_nested_list_of_enums = d.pop("nested_list_of_enums", UNSET)
407-
for nested_list_of_enums_item_data in _nested_list_of_enums or []:
408-
nested_list_of_enums_item = []
409-
_nested_list_of_enums_item = nested_list_of_enums_item_data
410-
for nested_list_of_enums_item_item_data in _nested_list_of_enums_item:
411-
nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data)
406+
nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET
407+
if _nested_list_of_enums is not UNSET:
408+
nested_list_of_enums = []
409+
for nested_list_of_enums_item_data in _nested_list_of_enums:
410+
nested_list_of_enums_item = []
411+
_nested_list_of_enums_item = nested_list_of_enums_item_data
412+
for nested_list_of_enums_item_item_data in _nested_list_of_enums_item:
413+
nested_list_of_enums_item_item = DifferentEnum(nested_list_of_enums_item_item_data)
412414

413-
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
415+
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
414416

415-
nested_list_of_enums.append(nested_list_of_enums_item)
417+
nested_list_of_enums.append(nested_list_of_enums_item)
416418

417419
_a_not_required_date = d.pop("a_not_required_date", UNSET)
418420
a_not_required_date: datetime.date | Unset

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,14 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
4444
from ..models.validation_error import ValidationError
4545

4646
d = dict(src_dict)
47-
detail = []
4847
_detail = d.pop("detail", UNSET)
49-
for detail_item_data in _detail or []:
50-
detail_item = ValidationError.from_dict(detail_item_data)
48+
detail: list[ValidationError] | Unset = UNSET
49+
if _detail is not UNSET:
50+
detail = []
51+
for detail_item_data in _detail:
52+
detail_item = ValidationError.from_dict(detail_item_data)
5153

52-
detail.append(detail_item)
54+
detail.append(detail_item)
5355

5456
http_validation_error = cls(
5557
detail=detail,

end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/a_model.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,19 @@ def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
8686
else:
8787
an_optional_allof_enum = check_an_all_of_enum(_an_optional_allof_enum)
8888

89-
nested_list_of_enums = []
9089
_nested_list_of_enums = d.pop("nested_list_of_enums", UNSET)
91-
for nested_list_of_enums_item_data in _nested_list_of_enums or []:
92-
nested_list_of_enums_item = []
93-
_nested_list_of_enums_item = nested_list_of_enums_item_data
94-
for nested_list_of_enums_item_item_data in _nested_list_of_enums_item:
95-
nested_list_of_enums_item_item = check_different_enum(nested_list_of_enums_item_item_data)
90+
nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET
91+
if _nested_list_of_enums is not UNSET:
92+
nested_list_of_enums = []
93+
for nested_list_of_enums_item_data in _nested_list_of_enums:
94+
nested_list_of_enums_item = []
95+
_nested_list_of_enums_item = nested_list_of_enums_item_data
96+
for nested_list_of_enums_item_item_data in _nested_list_of_enums_item:
97+
nested_list_of_enums_item_item = check_different_enum(nested_list_of_enums_item_item_data)
9698

97-
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
99+
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
98100

99-
nested_list_of_enums.append(nested_list_of_enums_item)
101+
nested_list_of_enums.append(nested_list_of_enums_item)
100102

101103
a_model = cls(
102104
an_enum_value=an_enum_value,

end_to_end_tests/literal-enums-golden-record/my_enum_api_client/models/post_user_list_body.py

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -158,33 +158,37 @@ def to_multipart(self) -> types.RequestFiles:
158158
@classmethod
159159
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
160160
d = dict(src_dict)
161-
an_enum_value = []
162161
_an_enum_value = d.pop("an_enum_value", UNSET)
163-
for an_enum_value_item_data in _an_enum_value or []:
164-
an_enum_value_item = check_an_enum(an_enum_value_item_data)
162+
an_enum_value: list[AnEnum] | Unset = UNSET
163+
if _an_enum_value is not UNSET:
164+
an_enum_value = []
165+
for an_enum_value_item_data in _an_enum_value:
166+
an_enum_value_item = check_an_enum(an_enum_value_item_data)
165167

166-
an_enum_value.append(an_enum_value_item)
168+
an_enum_value.append(an_enum_value_item)
167169

168-
an_enum_value_with_null = []
169170
_an_enum_value_with_null = d.pop("an_enum_value_with_null", UNSET)
170-
for an_enum_value_with_null_item_data in _an_enum_value_with_null or []:
171+
an_enum_value_with_null: list[AnEnumWithNull | None] | Unset = UNSET
172+
if _an_enum_value_with_null is not UNSET:
173+
an_enum_value_with_null = []
174+
for an_enum_value_with_null_item_data in _an_enum_value_with_null:
171175

172-
def _parse_an_enum_value_with_null_item(data: object) -> AnEnumWithNull | None:
173-
if data is None:
174-
return data
175-
try:
176-
if not isinstance(data, str):
177-
raise TypeError()
178-
componentsschemas_an_enum_with_null_type_1 = check_an_enum_with_null(data)
176+
def _parse_an_enum_value_with_null_item(data: object) -> AnEnumWithNull | None:
177+
if data is None:
178+
return data
179+
try:
180+
if not isinstance(data, str):
181+
raise TypeError()
182+
componentsschemas_an_enum_with_null_type_1 = check_an_enum_with_null(data)
179183

180-
return componentsschemas_an_enum_with_null_type_1
181-
except: # noqa: E722
182-
pass
183-
return cast(AnEnumWithNull | None, data)
184+
return componentsschemas_an_enum_with_null_type_1
185+
except: # noqa: E722
186+
pass
187+
return cast(AnEnumWithNull | None, data)
184188

185-
an_enum_value_with_null_item = _parse_an_enum_value_with_null_item(an_enum_value_with_null_item_data)
189+
an_enum_value_with_null_item = _parse_an_enum_value_with_null_item(an_enum_value_with_null_item_data)
186190

187-
an_enum_value_with_null.append(an_enum_value_with_null_item)
191+
an_enum_value_with_null.append(an_enum_value_with_null_item)
188192

189193
an_enum_value_with_only_null = cast(list[None], d.pop("an_enum_value_with_only_null", UNSET))
190194

@@ -202,17 +206,19 @@ def _parse_an_enum_value_with_null_item(data: object) -> AnEnumWithNull | None:
202206
else:
203207
an_optional_allof_enum = check_an_all_of_enum(_an_optional_allof_enum)
204208

205-
nested_list_of_enums = []
206209
_nested_list_of_enums = d.pop("nested_list_of_enums", UNSET)
207-
for nested_list_of_enums_item_data in _nested_list_of_enums or []:
208-
nested_list_of_enums_item = []
209-
_nested_list_of_enums_item = nested_list_of_enums_item_data
210-
for nested_list_of_enums_item_item_data in _nested_list_of_enums_item:
211-
nested_list_of_enums_item_item = check_different_enum(nested_list_of_enums_item_item_data)
210+
nested_list_of_enums: list[list[DifferentEnum]] | Unset = UNSET
211+
if _nested_list_of_enums is not UNSET:
212+
nested_list_of_enums = []
213+
for nested_list_of_enums_item_data in _nested_list_of_enums:
214+
nested_list_of_enums_item = []
215+
_nested_list_of_enums_item = nested_list_of_enums_item_data
216+
for nested_list_of_enums_item_item_data in _nested_list_of_enums_item:
217+
nested_list_of_enums_item_item = check_different_enum(nested_list_of_enums_item_item_data)
212218

213-
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
219+
nested_list_of_enums_item.append(nested_list_of_enums_item_item)
214220

215-
nested_list_of_enums.append(nested_list_of_enums_item)
221+
nested_list_of_enums.append(nested_list_of_enums_item)
216222

217223
post_user_list_body = cls(
218224
an_enum_value=an_enum_value,

end_to_end_tests/test-3-1-golden-record/test_3_1_features_client/models/post_prefix_items_body.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,33 +53,37 @@ def to_dict(self) -> dict[str, Any]:
5353
@classmethod
5454
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
5555
d = dict(src_dict)
56-
prefix_items_and_items = []
5756
_prefix_items_and_items = d.pop("prefixItemsAndItems", UNSET)
58-
for prefix_items_and_items_item_data in _prefix_items_and_items or []:
57+
prefix_items_and_items: list[float | Literal["prefix"] | str] | Unset = UNSET
58+
if _prefix_items_and_items is not UNSET:
59+
prefix_items_and_items = []
60+
for prefix_items_and_items_item_data in _prefix_items_and_items:
5961

60-
def _parse_prefix_items_and_items_item(data: object) -> float | Literal["prefix"] | str:
61-
prefix_items_and_items_item_type_0 = cast(Literal["prefix"], data)
62-
if prefix_items_and_items_item_type_0 != "prefix":
63-
raise ValueError(
64-
f"prefixItemsAndItems_item_type_0 must match const 'prefix', got '{prefix_items_and_items_item_type_0}'"
65-
)
66-
return prefix_items_and_items_item_type_0
67-
return cast(float | Literal["prefix"] | str, data)
62+
def _parse_prefix_items_and_items_item(data: object) -> float | Literal["prefix"] | str:
63+
prefix_items_and_items_item_type_0 = cast(Literal["prefix"], data)
64+
if prefix_items_and_items_item_type_0 != "prefix":
65+
raise ValueError(
66+
f"prefixItemsAndItems_item_type_0 must match const 'prefix', got '{prefix_items_and_items_item_type_0}'"
67+
)
68+
return prefix_items_and_items_item_type_0
69+
return cast(float | Literal["prefix"] | str, data)
6870

69-
prefix_items_and_items_item = _parse_prefix_items_and_items_item(prefix_items_and_items_item_data)
71+
prefix_items_and_items_item = _parse_prefix_items_and_items_item(prefix_items_and_items_item_data)
7072

71-
prefix_items_and_items.append(prefix_items_and_items_item)
73+
prefix_items_and_items.append(prefix_items_and_items_item)
7274

73-
prefix_items_only = []
7475
_prefix_items_only = d.pop("prefixItemsOnly", UNSET)
75-
for prefix_items_only_item_data in _prefix_items_only or []:
76+
prefix_items_only: list[float | str] | Unset = UNSET
77+
if _prefix_items_only is not UNSET:
78+
prefix_items_only = []
79+
for prefix_items_only_item_data in _prefix_items_only:
7680

77-
def _parse_prefix_items_only_item(data: object) -> float | str:
78-
return cast(float | str, data)
81+
def _parse_prefix_items_only_item(data: object) -> float | str:
82+
return cast(float | str, data)
7983

80-
prefix_items_only_item = _parse_prefix_items_only_item(prefix_items_only_item_data)
84+
prefix_items_only_item = _parse_prefix_items_only_item(prefix_items_only_item_data)
8185

82-
prefix_items_only.append(prefix_items_only_item)
86+
prefix_items_only.append(prefix_items_only_item)
8387

8488
post_prefix_items_body = cls(
8589
prefix_items_and_items=prefix_items_and_items,

0 commit comments

Comments
 (0)