diff --git a/task-sdk/src/airflow/sdk/serde/__init__.py b/task-sdk/src/airflow/sdk/serde/__init__.py index 0d94ebc328238..f671545d3aa29 100644 --- a/task-sdk/src/airflow/sdk/serde/__init__.py +++ b/task-sdk/src/airflow/sdk/serde/__init__.py @@ -285,7 +285,12 @@ def serialize(o: object, depth: int = 0) -> U | None: dct[DATA] = serialize(data, depth + 1) return dct - raise TypeError(f"cannot serialize object of type {cls}") + raise TypeError( + f"Cannot serialize object of type {cls}. Give it a `serialize()` method and a " + "`deserialize(data, version)` staticmethod, or decorate the class with @dataclass or " + "@attr.define. " + "See: https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/serializers.html" + ) def deserialize(o: T | None, full=True, type_hint: Any = None) -> object: @@ -387,7 +392,11 @@ def deserialize(o: T | None, full=True, type_hint: Any = None) -> object: return cls(**deserialize_value) # type: ignore[operator] # no deserializer available - raise TypeError(f"No deserializer found for {classname}") + raise TypeError( + f"No deserializer found for {classname}. It must provide a `deserialize(data, version)` " + "staticmethod (matching how it serializes), or be decorated with @dataclass or @attr.define. " + "See: https://airflow.apache.org/docs/apache-airflow/stable/authoring-and-scheduling/serializers.html" + ) def _convert(old: dict) -> dict: diff --git a/task-sdk/tests/task_sdk/serde/test_serde.py b/task-sdk/tests/task_sdk/serde/test_serde.py index 16d6851b40bd0..37d93a6e89578 100644 --- a/task-sdk/tests/task_sdk/serde/test_serde.py +++ b/task-sdk/tests/task_sdk/serde/test_serde.py @@ -297,7 +297,7 @@ def test_ser_namedtuple(self): def test_no_serializer(self): i = Exception - with pytest.raises(TypeError, match="^cannot serialize"): + with pytest.raises(TypeError, match="^Cannot serialize"): serialize(i) def test_ser_registered(self): @@ -523,8 +523,13 @@ def test_raise_undeserializable(self): "__version__": 0, } ) - with pytest.raises(TypeError, match="No deserializer"): + with pytest.raises(TypeError, match="No deserializer") as exc_info: deserialize(data) + assert ( + "It must provide a `deserialize(data, version)` staticmethod (matching how it " + "serializes), or be decorated with @dataclass or @attr.define." in str(exc_info.value) + ) + assert "authoring-and-scheduling/serializers.html" in str(exc_info.value) def test_backwards_compat(self): """ @@ -645,6 +650,11 @@ def test_error_when_serializing_callable_without_name(self): i = C() with pytest.raises( TypeError, - match="cannot serialize object of type ", - ): + match="Cannot serialize object of type ", + ) as exc_info: serialize(i) + assert ( + "Give it a `serialize()` method and a `deserialize(data, version)` staticmethod, or " + "decorate the class with @dataclass or @attr.define." in str(exc_info.value) + ) + assert "authoring-and-scheduling/serializers.html" in str(exc_info.value)