Skip to content

feat: add UnrecognizedEvent and raw_json() to the webhook event - #654

Closed
razor-x wants to merge 2 commits into
claude/revert-malformed-payload-tolerancefrom
claude/unrecognized-event-and-raw-json
Closed

feat: add UnrecognizedEvent and raw_json() to the webhook event#654
razor-x wants to merge 2 commits into
claude/revert-malformed-payload-tolerancefrom
claude/unrecognized-event-and-raw-json

Conversation

@razor-x

@razor-x razor-x commented Sep 2, 2026

Copy link
Copy Markdown
Member

Stacked on #653. Retarget to main once that merges.

The hole

An event whose event_type this SDK version does not know deserialized to a DeepAttrDict, where a missing key raises AttributeError rather than reading as None:

ev = seam_event_from_dict({"event_id": "e", "event_type": "future.thing"})
ev.event_id      # 'e'
ev.event_type    # 'future.thing'
ev.workspace_id  # AttributeError   <-- explodes
ev.created_at    # AttributeError
ev.occurred_at   # AttributeError

A handler reading a field every event shares worked for every event type the SDK knows and threw on the ones it does not — exactly backwards. Python was the only SDK with this behavior; Ruby, PHP, C# and JS all return null, because their fallback is a class declaring the shared event fields.

The fix

UnrecognizedEvent, built from the generic event resource the blueprint already carries, with every shared field optional. UnrecognizedActionAttempt does the same for an unrecognized action_type or status — there is no generic action_attempt resource in the blueprint, so it is built from the properties every variant agrees on.

ev.workspace_id  # None
isinstance(ev, UnrecognizedEvent)  # True

Why raw_json() is in the same PR

This drops ad-hoc access to fields the SDK does not know, which DeepAttrDict allowed. Three existing tests asserted exactly that (event.future_field.nested, unknown.future_api_field, event.foo.bar) and are updated here.

That trade is intended — writing logic against a new field is what an upgrade is for — but it would be a pure capability loss without a way to reach the payload. So events also gain raw_json():

json.loads(event.raw_json())["a_field_this_version_predates"]

Scoped to events, for the webhook verify return; a test asserts Device does not have it. A method rather than a property because the call is where the serialization happens. The retained payload is repr=False and compare=False, so it changes neither the repr nor equality.

What is deliberately not changed

The declared SeamEvent and ActionAttempt unions. Adding an all-optional member would make every field on them optional for type checkers — action_attempt_id becoming str | None alone produced 14 mypy errors in this repo, and would produce more in callers. The fallback is returned via cast, and isinstance is how you detect it.

282 tests, mypy clean, pylint 10.00, black clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA


Generated by Claude Code

…ying raw_json

An event whose event_type this SDK version does not know deserialized to a
DeepAttrDict, where a missing key raises AttributeError rather than reading as
None. So a handler reading a field every event shares worked for every event
type the SDK knows and threw on the ones it does not:

    ev = seam_event_from_dict({"event_id": "e", "event_type": "future.thing"})
    ev.event_type    # 'future.thing'
    ev.workspace_id  # AttributeError

Every other Seam SDK returns null here, because their fallback is a class that
declares the shared event fields. Python now matches: UnrecognizedEvent, built
from the generic event resource in the blueprint, with every shared field
optional. UnrecognizedActionAttempt does the same for an unrecognized
action_type or status, built from the properties every variant agrees on.

This drops ad-hoc access to fields the SDK does not know, which the previous
DeepAttrDict allowed. That is the intended trade -- writing logic against a new
field is what an upgrade is for -- but it means the payload has to stay
reachable, so events also gain raw_json():

    json.loads(event.raw_json())["a_field_this_version_predates"]

Scoped to events, for the webhook verify return; a test asserts Device does not
have it. The retained payload is repr=False and compare=False, so it changes
neither the repr nor equality.

The declared SeamEvent and ActionAttempt unions are unchanged. Adding an
all-optional member would make every field on them optional for type checkers,
which costs callers more than the honest union buys them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
@razor-x
razor-x requested a review from a team as a code owner September 2, 2026 06:34
@razor-x razor-x closed this Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants