Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions serde/se.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,7 @@ def wrap(cls: type[T]) -> type[T]:
add_func(scope, union_key, render_union_func(cls, union_args, tagging), g)
scope.union_se_args[union_key] = union_args

for f in sefields(cls, serialize_class_var):
if f.skip_if:
g[f.skip_if.name] = f.skip_if
if f.serializer:
g[f.serializer.name] = f.serializer
add_field_serializers_to_scope(sefields(cls, serialize_class_var), g, serialize_class_var)

add_func(
scope,
Expand Down Expand Up @@ -603,6 +599,29 @@ def sefields(cls: type[Any], serialize_class_var: bool = False) -> Iterator[SeFi
yield f


def add_field_serializers_to_scope(
sfields: Iterator[SeField[Any]], g: dict[str, Any], serialize_class_var: bool = False
) -> None:
"""
Register custom field serializers and skip_if predicates in the generated globals.

A flattened dataclass field is rendered inline in the enclosing class's serialize
function, so the custom serializers and skip_if predicates of its inner fields must
be available in this scope too, not only those of the top-level fields (#453).
"""
for f in sfields:
if f.skip_if:
g[f.skip_if.name] = f.skip_if
if f.serializer:
g[f.serializer.name] = f.serializer
if f.flatten:
inner = f[0] if is_opt(f.type) else f
if is_dataclass(inner.type):
add_field_serializers_to_scope(
sefields(inner.type, serialize_class_var), g, serialize_class_var
)


jinja2_env = jinja2.Environment(
loader=jinja2.DictLoader(
{
Expand Down
21 changes: 21 additions & 0 deletions tests/test_flatten.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,27 @@ class Foo:
assert from_json(Foo, s) == f


def test_flatten_custom_field_serializer() -> None:
# A custom field serializer on a flattened dataclass's field must be in
# scope of the enclosing class's generated serializer (#453).
@serde
class Child:
value: int = field(serializer=str)

@serde
class Parent:
child: Child = field(flatten=True)

assert to_json(Parent(child=Child(value=3))) == '{"value":"3"}'

# also works through a nested flatten
@serde
class GrandParent:
parent: Parent = field(flatten=True)

assert to_json(GrandParent(parent=Parent(child=Child(value=7)))) == '{"value":"7"}'


@pytest.mark.parametrize("se,de", all_formats)
def test_flatten(se: Any, de: Any) -> None:
@serde
Expand Down
Loading