diff --git a/serde/de.py b/serde/de.py index 01479390..cf242564 100644 --- a/serde/de.py +++ b/serde/de.py @@ -737,7 +737,11 @@ def __getitem__(self, n: int) -> DeField[Any] | InnerField[Any]: return InnerField(typ, f"{self.data}[{n}]", datavar=f"{self.data}[{n}]", **opts) else: # For Optional etc. - return DeField( + # Preserve InnerField when unwrapping Optional inside a collection, + # so that data access uses the loop variable directly (e.g. "v") + # instead of indexing into it (e.g. v["v"]). + field_cls = type(self) + return field_cls( typ, self.name, datavar=self.datavar, @@ -1001,7 +1005,7 @@ def opt(self, arg: DeField[Any]) -> str: Render rvalue for Optional. """ inner = arg[0] - if arg.iterbased: + if arg.iterbased or isinstance(arg, InnerField): exists = f"{arg.data} is not None" elif arg.flatten: # Check nullabilities of all nested fields. diff --git a/tests/test_union.py b/tests/test_union.py index 195ab695..04bbefb0 100644 --- a/tests/test_union.py +++ b/tests/test_union.py @@ -919,3 +919,20 @@ class Baz: all_false_json = '{"num": {"all": false}}' with pytest.raises(SerdeError): from_json(Baz, all_false_json) + + +def test_optional_value_in_list(): + """Test that Optional/Union with None in list values deserializes correctly.""" + + @serde + @dataclass + class Inner: + value: int = 10 + + @serde + @dataclass + class Outer: + items: list[Optional[Inner]] + + instance = Outer(items=[Inner(value=1), None, Inner(value=3)]) + assert from_dict(Outer, to_dict(instance)) == instance