From 41c7397b9b87b479ea272d0f679d7c6538ad367f Mon Sep 17 00:00:00 2001 From: Eryx-Z <257642323+Eryx-Z@users.noreply.github.com> Date: Sat, 17 Oct 2026 04:43:50 +0800 Subject: [PATCH] fix: treat dataclass default_factory as a default in union disambiguation create_default_dis_func used fields_dict, so a dataclass Field with only default_factory kept .default as MISSING and was treated as required. That made omit_if_default payloads with those fields silently pick the other union member. Use adapted_fields, which maps default_factory to Factory, matching attrs Factory and plain dataclass defaults. Fixes #796 --- src/cattrs/disambiguators.py | 9 ++++++--- tests/test_disambiguators.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/cattrs/disambiguators.py b/src/cattrs/disambiguators.py index 451d5768..62f5e483 100644 --- a/src/cattrs/disambiguators.py +++ b/src/cattrs/disambiguators.py @@ -14,7 +14,6 @@ from ._compat import ( NoneType, adapted_fields, - fields_dict, get_args, has, is_literal, @@ -150,8 +149,12 @@ def dis_func(data: Mapping[Any, Any]) -> type | None: ) uniq = cl_reqs - other_reqs - # We want a unique attribute with no default. - cl_fields = fields_dict(get_origin(cl) or cl) + # We want a unique attribute with no default. Use adapted_fields so + # dataclass default_factory is treated as a default (via Factory), + # matching attrs Factory and plain dataclass default. + cl_fields = { + a.name: a for a in adapted_fields(get_origin(cl) or cl) + } for maybe_renamed_attr_name in uniq: orig_name = back_map[maybe_renamed_attr_name] if cl_fields[orig_name].default in (NOTHING, MISSING): diff --git a/tests/test_disambiguators.py b/tests/test_disambiguators.py index 66f366d8..e39ae244 100644 --- a/tests/test_disambiguators.py +++ b/tests/test_disambiguators.py @@ -495,3 +495,20 @@ class B: assert converter.structure({"a": "a"}, Union[A, B]) == A() assert converter.structure({"b": "b"}, Union[A, B]) == B("b") + + +def test_dataclass_default_factory_not_required(): + """Dataclass default_factory fields count as defaults for disambiguation.""" + import dataclasses + + @dataclass + class D1: + x: int + tags: list = dataclasses.field(default_factory=list) + + @dataclass + class D2: + x: int + + with pytest.raises(TypeError, match="Could not disambiguate"): + create_default_dis_func(Converter(), D1, D2)