diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000000..4af76f652b --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,5 @@ +Release type: patch + +`strawberry.asdict` now correctly handles `Some` and `UNSET` values. +`Some(value)` is unwrapped to its inner value, and fields set to `UNSET` +are excluded from the resulting dictionary. diff --git a/strawberry/types/object_type.py b/strawberry/types/object_type.py index 56c283574f..2e43f358e5 100644 --- a/strawberry/types/object_type.py +++ b/strawberry/types/object_type.py @@ -465,7 +465,11 @@ class MyNode: def asdict(obj: Any) -> dict[str, object]: """Convert a strawberry object into a dictionary. - This wraps the dataclasses.asdict function to strawberry. + This wraps the dataclasses.asdict function with additional handling + for strawberry-specific types like ``Some`` and ``UNSET``. + + - Fields set to ``UNSET`` are excluded from the result. + - ``Some(value)`` wrappers are unwrapped to their inner value. Args: obj: The object to convert into a dictionary. @@ -486,7 +490,28 @@ class User: # {"name": "Lorem", "age": 25} ``` """ - return dataclasses.asdict(obj) + from strawberry.types.maybe import Some + from strawberry.types.unset import UNSET + + def _asdict_inner(obj: Any) -> Any: + if isinstance(obj, Some): + return _asdict_inner(obj.value) + if dataclasses.is_dataclass(obj) and not isinstance(obj, builtins.type): + result = {} + for f in dataclasses.fields(obj): + value = getattr(obj, f.name) + if value is UNSET: + continue + result[f.name] = _asdict_inner(value) + return result + if isinstance(obj, (list, tuple)): + cls = builtins.type(obj) + return cls(_asdict_inner(v) for v in obj) + if isinstance(obj, dict): + return {_asdict_inner(k): _asdict_inner(v) for k, v in obj.items()} + return obj + + return _asdict_inner(obj) __all__ = [ diff --git a/tests/types/test_convert_to_dictionary.py b/tests/types/test_convert_to_dictionary.py index b35920b081..7e982f9ff0 100644 --- a/tests/types/test_convert_to_dictionary.py +++ b/tests/types/test_convert_to_dictionary.py @@ -1,7 +1,8 @@ from enum import Enum import strawberry -from strawberry import asdict +from strawberry import UNSET, asdict +from strawberry.types.maybe import Some def test_convert_simple_type_to_dictionary(): @@ -62,3 +63,62 @@ class QnaInput: "description": description, "tags": None, } + + +def test_convert_some_values_are_unwrapped(): + @strawberry.type + class User: + name: str + age: strawberry.Maybe[int] + + user = User(name="Alex", age=Some(30)) + + assert asdict(user) == { + "name": "Alex", + "age": 30, + } + + +def test_convert_unset_fields_are_excluded(): + @strawberry.input + class UserInput: + name: str + age: int | None = UNSET + + user = UserInput(name="Alex") + + assert asdict(user) == { + "name": "Alex", + } + + +def test_convert_some_none_is_preserved(): + @strawberry.type + class User: + name: str + age: strawberry.Maybe[int] + + user = User(name="Alex", age=Some(None)) + + assert asdict(user) == { + "name": "Alex", + "age": None, + } + + +def test_convert_nested_some_values(): + @strawberry.type + class Address: + city: str + + @strawberry.type + class User: + name: str + address: strawberry.Maybe[Address] + + user = User(name="Alex", address=Some(Address(city="NYC"))) + + assert asdict(user) == { + "name": "Alex", + "address": {"city": "NYC"}, + }