From 1bdc3398ac281f4193a9d4d14e34395405f81af2 Mon Sep 17 00:00:00 2001 From: Amogh Desai Date: Mon, 3 Aug 2026 14:21:52 +0530 Subject: [PATCH 1/2] Make serde errors for unsupported types a bit more actionable --- task-sdk/src/airflow/sdk/serde/__init__.py | 12 ++++++++++-- task-sdk/tests/task_sdk/serde/test_serde.py | 8 ++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/task-sdk/src/airflow/sdk/serde/__init__.py b/task-sdk/src/airflow/sdk/serde/__init__.py index 0d94ebc328238..805c7ccc7961e 100644 --- a/task-sdk/src/airflow/sdk/serde/__init__.py +++ b/task-sdk/src/airflow/sdk/serde/__init__.py @@ -285,7 +285,11 @@ 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 matching " + "`deserialize(data, version)`), 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 +391,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)` " + "classmethod (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..7fc58d7b6f0eb 100644 --- a/task-sdk/tests/task_sdk/serde/test_serde.py +++ b/task-sdk/tests/task_sdk/serde/test_serde.py @@ -523,8 +523,10 @@ 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 "deserialize(data, version)" in str(exc_info.value) + assert "authoring-and-scheduling/serializers.html" in str(exc_info.value) def test_backwards_compat(self): """ @@ -646,5 +648,7 @@ def test_error_when_serializing_callable_without_name(self): with pytest.raises( TypeError, match="cannot serialize object of type ", - ): + ) as exc_info: serialize(i) + assert "@dataclass or @attr.define" in str(exc_info.value) + assert "authoring-and-scheduling/serializers.html" in str(exc_info.value) From 86d835f9b82a75905a145f1291ddfcf5e2f6c9f6 Mon Sep 17 00:00:00 2001 From: Amogh Desai Date: Mon, 3 Aug 2026 15:00:36 +0530 Subject: [PATCH 2/2] Make serde errors for unsupported types a bit more actionable --- task-sdk/src/airflow/sdk/serde/__init__.py | 7 ++++--- task-sdk/tests/task_sdk/serde/test_serde.py | 14 ++++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/task-sdk/src/airflow/sdk/serde/__init__.py b/task-sdk/src/airflow/sdk/serde/__init__.py index 805c7ccc7961e..f671545d3aa29 100644 --- a/task-sdk/src/airflow/sdk/serde/__init__.py +++ b/task-sdk/src/airflow/sdk/serde/__init__.py @@ -286,8 +286,9 @@ def serialize(o: object, depth: int = 0) -> U | None: return dct raise TypeError( - f"cannot serialize object of type {cls}. Give it a `serialize()` method (and a matching " - "`deserialize(data, version)`), or decorate the class with @dataclass or @attr.define. " + 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" ) @@ -393,7 +394,7 @@ def deserialize(o: T | None, full=True, type_hint: Any = None) -> object: # no deserializer available raise TypeError( f"No deserializer found for {classname}. It must provide a `deserialize(data, version)` " - "classmethod (matching how it serializes), or be decorated with @dataclass or @attr.define. " + "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" ) diff --git a/task-sdk/tests/task_sdk/serde/test_serde.py b/task-sdk/tests/task_sdk/serde/test_serde.py index 7fc58d7b6f0eb..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): @@ -525,7 +525,10 @@ def test_raise_undeserializable(self): ) with pytest.raises(TypeError, match="No deserializer") as exc_info: deserialize(data) - assert "deserialize(data, version)" in str(exc_info.value) + 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): @@ -647,8 +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 "@dataclass or @attr.define" in str(exc_info.value) + 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)