diff --git a/serde/compat.py b/serde/compat.py index b94c9df2..5a78a47a 100644 --- a/serde/compat.py +++ b/serde/compat.py @@ -513,6 +513,25 @@ def is_bare_opt(typ: Any) -> bool: return not type_args(typ) and typ is Optional +@cache +def is_opt_dataclass(typ: Any) -> bool: + """ + Test if the type is optional dataclass. + + >>> is_opt_dataclass(Optional[int]) + False + >>> @dataclasses.dataclass + ... class Foo: + ... pass + >>> is_opt_dataclass(Foo) + False + >>> is_opt_dataclass(Optional[Foo]) + False + """ + args = get_args(typ) + return is_opt(typ) and len(args) > 0 and is_dataclass(args[0]) + + @cache def is_list(typ: type[Any]) -> bool: """ diff --git a/serde/core.py b/serde/core.py index 0c15d50c..aed13013 100644 --- a/serde/core.py +++ b/serde/core.py @@ -41,6 +41,7 @@ is_new_type_primitive, is_any, is_opt, + is_opt_dataclass, is_set, is_tuple, is_union, @@ -620,6 +621,8 @@ def from_dataclass(cls, f: dataclasses.Field[T], parent: Optional[Any] = None) - flatten = f.metadata.get("serde_flatten") if flatten is True: flatten = FlattenOpts() + if flatten and not (dataclasses.is_dataclass(f.type) or is_opt_dataclass(f.type)): + raise SerdeError(f"pyserde does not support flatten attribute for {typename(f.type)}") kw_only = bool(f.kw_only) if sys.version_info >= (3, 10) else False diff --git a/tests/test_flatten.py b/tests/test_flatten.py index afcf99b7..85c0b390 100644 --- a/tests/test_flatten.py +++ b/tests/test_flatten.py @@ -6,7 +6,7 @@ import pytest -from serde import field, serde +from serde import field, serde, SerdeError from serde.json import from_json, to_json from .common import all_formats @@ -68,3 +68,16 @@ class Foo: f = Foo(a=10, b="foo", bar=Bar(c=100.0, d=True)) assert de(Foo, se(f)) == f + + +@pytest.mark.parametrize("se,de", all_formats) +def test_flatten_not_supported(se: Any, de: Any) -> None: + @serde + class Bar: + pass + + with pytest.raises(SerdeError): + + @serde + class Foo: + bar: list[Bar] = field(flatten=True)