Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [Fix union matches using .popitem() when no matches are found](https://github.com/konradhalas/dacite/pull/233)
- [Supress unnecessary context in some stack trackes](https://github.com/konradhalas/dacite/pull/265)
- Fix issues with caching internal function calls
- Improve non-strict dataclasses union match

## [1.8.1] - 2023-05-12

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ exception.
### Strict unions match

`Union` allows to define multiple possible types for a given field. By default
`dacite` is trying to find the first matching type for a provided data and it
`dacite` is trying to find the best matching type for a provided data by number of matching fields and it
returns instance of this type. It means that it's possible that there are other
matching types further on the `Union` types list. With `strict_unions_match`
only a single match is allowed, otherwise `dacite` raises `StrictUnionMatchError`.
Expand Down
21 changes: 13 additions & 8 deletions dacite/core.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import is_dataclass
from functools import partial
from itertools import zip_longest
from typing import TypeVar, Type, Optional, Mapping, Any, Collection, MutableMapping

Expand Down Expand Up @@ -128,21 +129,25 @@ def _build_value_for_union(union: Type, data: Any, config: Config) -> Any:
except Exception: # pylint: disable=broad-except
continue
if is_instance(value, inner_type):
if config.strict_unions_match:
union_matches[inner_type] = value
else:
return value
union_matches[inner_type] = value
except DaciteError:
pass
if config.strict_unions_match and union_matches:
if len(union_matches) > 1:
raise StrictUnionMatchError(union_matches)
return union_matches.popitem()[1]
if len(union_matches) > 1 and config.strict_unions_match:
raise StrictUnionMatchError(union_matches)
if union_matches:
return union_matches[sorted(union_matches.keys(), key=partial(_field_key_matches, data))[-1]]
if not config.check_types:
return data
raise UnionMatchError(field_type=union, value=data)


def _field_key_matches(data: Any, inner_type: Type) -> int:
if not is_dataclass(inner_type):
return 0
data_class_fields = cache(get_fields)(inner_type)
return len(set(data.keys()) & {f.name for f in data_class_fields})


def _build_value_for_collection(collection: Type, data: Any, config: Config) -> Any:
data_type = data.__class__
if isinstance(data, Mapping) and is_subclass(collection, Mapping):
Expand Down
23 changes: 23 additions & 0 deletions tests/core/test_union.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,26 @@ class Y:
result = from_dict(Y, {"d": {"x": {"i": 42}, "z": {"i": 37}}})

assert result == Y(d={"x": X(i=42), "z": X(i=37)})


def test_from_dict_with_union_of_data_classes_selects_type_by_number_of_matching_fields():
@dataclass
class X:
i: Optional[int]

@dataclass
class Y:
j: Optional[int]

@dataclass
class Z:
j: int
k: int

@dataclass
class A:
d: Union[X, Y, Z]

result = from_dict(A, {"d": {"j": 42, "k": 42}})

assert result == A(d=Z(j=42, k=42))
Loading