Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions sdk/eggai/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
metadata and context as CloudEvents-compliant extension attributes.

The model is generic so that you can specify custom Pydantic models
for data, the application-specific event payload. ``data`` is required:

Check notice on line 23 in sdk/eggai/schemas.py

View workflow job for this annotation

GitHub Actions / QualOps Code Quality

LOW: bug

Class docstring still describes `data` as required and misdirects users to `Message` for empty-dict defaults, both of which are now false after this change.
a typed subclass (``BaseMessage[Order]``) then rejects envelopes whose
payload is missing or malformed instead of silently defaulting. For a
plain dict payload with an empty default, use :class:`Message`.
Expand Down Expand Up @@ -67,14 +67,14 @@
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,

Check warning on line 77 in sdk/eggai/schemas.py

View workflow job for this annotation

GitHub Actions / QualOps Code Quality

MEDIUM: bug

Adding `validate_default=True` to `BaseMessage.data` is a breaking change for existing consumers that instantiate `BaseMessage` or any typed subclass without providing `data`.
description="Event payload containing application-specific data.",
)

Expand Down
8 changes: 8 additions & 0 deletions sdk/tests/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,17 @@
{"source": "t", "type": "OrderMessage", "data": {"order_id": 7}}
)
assert isinstance(msg.data, Order)
assert msg.data.order_id == 7

Check notice on line 36 in sdk/tests/test_schemas.py

View workflow job for this annotation

GitHub Actions / QualOps Code Quality

LOW: bug

The `validate_default=True` protection against silent type confusion is tested only for named subclasses; inline generic parameterization (`BaseMessage[Order].model_validate(...)`) is not tested, leaving the safety guarantee unverified for that usage.


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")
Expand Down
Loading