Repository: yukinarit/pyserde (python)

Issue:
Flatten does not work with optional dataclass fields
Hi,
Currently trying to move over to using pyserde and dataclasses instead of dictionaries for more type assurance.
I have discovered what I think is a bug for when the `flatten` field attribute is used for fields with optional dataclasses as type.
This example
```python
import serde
from dataclasses import dataclass
from typing import Optional


@serde.serde
@dataclass
class SubStruct:
    sub_id: int
    value: int


@serde.serde
@dataclass
class Struct:
    id: Optional[int]
    sub: Optional[SubStruct] = serde.field(flatten=True)


print(serde.from_dict(Struct, {"id": 123, "sub_id": 69, "value": 420}))
```
I expected this to return `Struct(id=123, sub=SubStruct(sub_id=69, value=420))`.
It does return this if one changes the `sub` field to be `SubStruct` without the Optional.
However with the Optional present (as in the example above) the return is:
`Struct(id=123, sub=None)`

Am I misunderstanding/misusing something?

Thanks


Pull request:
Fix flatten for optional
Closes #328

Patch:
diff --git a/serde/de.py b/serde/de.py
index 6c49e5e..e51458c 100644
--- a/serde/de.py
+++ b/serde/de.py
@@ -584,13 +584,17 @@ class DeField(Field[T]):
         """
         typ = type_args(self.type)[n]
         opts: dict[str, Any] = {
+            "kw_only": self.kw_only,
             "case": self.case,
+            "alias": self.alias,
             "rename": self.rename,
             "skip": self.skip,
             "skip_if": self.skip_if,
             "skip_if_false": self.skip_if_false,
+            "skip_if_default": self.skip_if_default,
+            "serializer": self.serializer,
+            "deserializer": self.deserializer,
             "flatten": self.flatten,
-            "alias": self.alias,
             "parent": self.parent,
         }
         if is_list(self.type) or is_dict(self.type) or is_set(self.type):
@@ -856,9 +860,17 @@ class Renderer:
 maybe_generic_type_vars=maybe_generic_type_vars, variable_type_args=None, \
 reuse_instances=reuse_instances)) if data.get("f") is not None else None'
         """
-        value_arg = arg[0]
+        inner = arg[0]
         if arg.iterbased:
             exists = f"{arg.data} is not None"
+        elif arg.flatten:
+            # Check nullabilities of all nested fields.
+            exists = " and ".join(
+                [
+                    f'{arg.datavar}.get("{f.name}") is not None'
+                    for f in dataclasses.fields(inner.type)
+                ]
+            )
         else:
             name = arg.conv_name()
             if arg.alias:
@@ -867,7 +879,7 @@ reuse_instances=reuse_instances)) if data.get("f") is not None else None'
             else:
                 get = f'{arg.datavar}.get("{name}")'
             exists = f"{get} is not None"
-        return f"({self.render(value_arg)}) if {exists} else None"
+        return f"({self.render(inner)}) if {exists} else None"
 
     def list(self, arg: DeField[Any]) -> str:
         """
diff --git a/serde/se.py b/serde/se.py
index 44a7192..8bf4938 100644
--- a/serde/se.py
+++ b/serde/se.py
@@ -480,7 +480,20 @@ class SeField(Field[T]):
 
     def __getitem__(self, n: int) -> SeField[Any]:
         typ = type_args(self.type)[n]
-        return SeField(typ, name=None)
+        opts: dict[str, Any] = {
+            "kw_only": self.kw_only,
+            "case": self.case,
+            "alias": self.alias,
+            "rename": self.rename,
+            "skip": self.skip,
+            "skip_if": self.skip_if,
+            "skip_if_false": self.skip_if_false,
+            "skip_if_default": self.skip_if_default,
+            "serializer": self.serializer,
+            "deserializer": self.deserializer,
+            "flatten": self.flatten,
+        }
+        return SeField(typ, name=None, **opts)
 
 
 def sefields(cls: type[Any], serialize_class_var: bool = False) -> Iterator[SeField[Any]]:
@@ -654,8 +667,12 @@ class LRenderer:
         """
         if is_dataclass(arg.type) and arg.flatten:
             return self.flatten(arg)
-        else:
-            return f'res["{arg.conv_name(self.case)}"]'
+        elif is_opt(arg.type) and arg.flatten:
+            inner = arg[0]
+            if is_dataclass(inner.type):
+                return self.flatten(inner)
+
+        return f'res["{arg.conv_name(self.case)}"]'
 
     def flatten(self, arg: SeField[Any]) -> str:
         """
@@ -818,9 +835,12 @@ coerce_object(int, foo[2]),)"
         """
         if is_bare_opt(arg.type):
             return f"{arg.varname} if {arg.varname} is not None else None"
+
+        inner = arg[0]
+        inner.name = arg.varname
+        if arg.flatten:
+            return self.render(inner)
         else:
-            inner = arg[0]
-            inner.name = arg.varname
             return f"({self.render(inner)}) if {arg.varname} is not None else None"
 
     def list(self, arg: SeField[Any]) -> str:


Test patch:
diff --git a/tests/test_flatten.py b/tests/test_flatten.py
index b0583f9..afcf99b 100644
--- a/tests/test_flatten.py
+++ b/tests/test_flatten.py
@@ -2,7 +2,7 @@
 Tests for flatten attribute.
 """
 
-from typing import Any
+from typing import Any, Optional
 
 import pytest
 
@@ -51,3 +51,20 @@ def test_flatten(se: Any, de: Any) -> None:
 
     f = Foo(a=10, b="foo", bar=Bar(c=100.0, d=True, baz=Baz([1, 2], {"a": "10"})))
     assert de(Foo, se(f)) == f
+
+
+@pytest.mark.parametrize("se,de", all_formats)
+def test_flatten_optional(se: Any, de: Any) -> None:
+    @serde
+    class Bar:
+        c: float
+        d: bool
+
+    @serde
+    class Foo:
+        a: int
+        b: str
+        bar: Optional[Bar] = field(flatten=True)
+
+    f = Foo(a=10, b="foo", bar=Bar(c=100.0, d=True))
+    assert de(Foo, se(f)) == f