From d53b7c1b39b46022c19cd010e766903c862e9e2a Mon Sep 17 00:00:00 2001 From: Stefano Date: Mon, 7 Sep 2026 17:05:52 +0200 Subject: [PATCH] fix: restore BaseMessage.data default, validated 0.4.0 made data required to stop typed subclasses from silently accepting a missing payload (pydantic does not validate defaults). validate_default=True gives the same protection: the {} default fails validation for BaseMessage[Order] and passes for the untyped base, so BaseMessage(source=..., type=...) works again. --- sdk/CHANGELOG.md | 8 ++++++++ sdk/eggai/schemas.py | 14 +++++++------- sdk/tests/test_schemas.py | 8 ++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/sdk/CHANGELOG.md b/sdk/CHANGELOG.md index 82240b2e..ffaa2bee 100644 --- a/sdk/CHANGELOG.md +++ b/sdk/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- `BaseMessage.data` has its `{}` default back, now declared with + `validate_default=True`. 0.4.0 made the field required to stop typed + subclasses (`BaseMessage[Order]`) from silently accepting a missing payload; + validating the default achieves the same without breaking + `BaseMessage(source=..., type=...)` for the untyped base. Typed subclasses + still reject a missing or malformed payload with a `ValidationError`. + ## [0.4.0] - 2026-09-07 ### Added diff --git a/sdk/eggai/schemas.py b/sdk/eggai/schemas.py index dc0a1438..160d2832 100644 --- a/sdk/eggai/schemas.py +++ b/sdk/eggai/schemas.py @@ -67,14 +67,14 @@ class BaseMessage(BaseModel, Generic[TData]): default=None, description="W3C traceparent for distributed trace context propagation.", ) - # No default here: pydantic does not validate defaults, so a dict default on a - # field typed TData would hand every typed subclass a plain `{}` whenever an - # envelope omits `data` — the subclass's annotation says Order, the runtime - # value is a dict, and the handler crashes on first attribute access. Requiring - # the field turns that envelope into a ValidationError, which the transports' - # typed subscriptions already treat as "not ours: skip and ack". + # validate_default: pydantic does not validate defaults unless asked, so on a + # typed subclass (BaseMessage[Order]) a missing `data` used to become a plain + # `{}` labelled as Order. With validation the `{}` default fails for typed + # subclasses (ValidationError, which typed subscriptions treat as "not ours") + # and still works for the untyped base. data: TData = Field( - ..., + default_factory=dict, + validate_default=True, description="Event payload containing application-specific data.", ) diff --git a/sdk/tests/test_schemas.py b/sdk/tests/test_schemas.py index e0d50f7d..f1927cbe 100644 --- a/sdk/tests/test_schemas.py +++ b/sdk/tests/test_schemas.py @@ -36,6 +36,14 @@ def test_typed_subclass_accepts_valid_data(): assert msg.data.order_id == 7 +def test_untyped_base_message_defaults_to_empty_dict(): + # Only typed subclasses reject a missing payload; the generic base keeps + # working without one (0.4.0 briefly required it, restored in 0.4.1). + msg = BaseMessage(source="t", type="test.event") + assert msg.data == {} + assert BaseMessage.model_validate({"source": "t", "type": "test.event"}).data == {} + + def test_message_still_defaults_to_empty_dict(): # The dict default lives on the concrete Message, where it is honest. msg = Message(source="t", type="test.event")