diff --git a/pkgs/clan-cli/clan_cli/api/serde.py b/pkgs/clan-cli/clan_cli/api/serde.py index a3ff265d5..a82df0dc8 100644 --- a/pkgs/clan-cli/clan_cli/api/serde.py +++ b/pkgs/clan-cli/clan_cli/api/serde.py @@ -127,6 +127,11 @@ def construct_value(t: type, field_value: JsonValue, loc: list[str] = []) -> Any """ if t is None and field_value: raise ClanError(f"Expected None but got: {field_value}", location=f"{loc}") + + if is_type_in_union(t, type(None)) and field_value is None: + # Sometimes the field value is None, which is valid if the type hint allows None + return None + # If the field is another dataclass # Field_value must be a dictionary if is_dataclass(t) and isinstance(field_value, dict): @@ -160,9 +165,9 @@ def construct_value(t: type, field_value: JsonValue, loc: list[str] = []) -> Any # Union types construct the first non-None type elif is_union_type(t): # Unwrap the union type - t = unwrap_none_type(t) + inner = unwrap_none_type(t) # Construct the field value - return construct_value(t, field_value) + return construct_value(inner, field_value) # Nested types # list diff --git a/pkgs/clan-cli/tests/test_deserializers.py b/pkgs/clan-cli/tests/test_deserializers.py index b475c00b9..7bcca85de 100644 --- a/pkgs/clan-cli/tests/test_deserializers.py +++ b/pkgs/clan-cli/tests/test_deserializers.py @@ -244,6 +244,27 @@ def test_alias_field_from_orig_name() -> None: from_dict(Person, data) +def test_none_or_string() -> None: + """ + Field declares an alias. But the data is provided with the field name. + """ + + data = None + + @dataclass + class Person: + name: Path + + checked = from_dict(str | None, data) + assert checked is None + + checked2 = from_dict(dict[str, str] | None, data) + assert checked2 is None + + checked3 = from_dict(Person | None, data) + assert checked3 is None + + def test_path_field() -> None: @dataclass class Person: