Skip to content

Commit bc9bd7b

Browse files
committed
feat: add raw_json() to the event returned by webhook verification
from_dict reads only the properties it was generated for, so a field Seam adds to an existing event between SDK releases is silently discarded. #651 made parsing degrade instead of raise, but did not keep the payload, so there was no way to reach a new field short of upgrading. SeamWebhook.verify() now returns an event carrying raw_json(): json.loads(event.raw_json())["a_field_this_version_predates"] DeepAttrDict answers the same call, so the unrecognized-event branch and the typed branch are handled the same way. This matters because the fallback is not otherwise interchangeable with a generated event: reading a missing key raises AttributeError where a dataclass returns None, so generic code written against the declared union breaks on it. Scoped to events. It is there for the verify return, not as a general accessor on every model. A method rather than a property because the call is where the serialization happens; the event retains the decoded payload and serializes on demand. The retained payload is repr=False and compare=False, so it changes neither the repr nor equality. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2kJ4nGaM8imZCVMEKjmXA
1 parent 4bab5aa commit bc9bd7b

6 files changed

Lines changed: 726 additions & 5 deletions

File tree

codegen/layouts/partials/resource-dataclass.hbs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@
2525
{{#each properties}}
2626
{{../memberIndent}}{{pythonIdentifier name}}: {{type}}
2727
{{/each}}
28+
{{#if @root.hasRawJson}}
29+
{{#unless isNested}}
30+
{{memberIndent}}_raw: Optional[Dict[str, Any]] = field(default=None, repr=False, compare=False)
31+
32+
{{memberIndent}}def raw_json(self) -> str:
33+
{{memberIndent}} """Return the payload this was parsed from, as JSON."""
34+
{{memberIndent}} return json.dumps(self._raw)
35+
{{/unless}}
36+
{{/if}}
2837

2938
{{memberIndent}}@classmethod
3039
{{memberIndent}}def from_dict(cls, d: Any):
@@ -37,4 +46,9 @@
3746
{{#each properties}}
3847
{{../memberIndent}} {{pythonIdentifier name}}={{#if isRequiredObject}}_required_object_from_dict(cls.{{nestedClassName}}, d.get("{{name}}")){{else}}{{#if isObject}}_object_from_dict(cls.{{nestedClassName}}, d.get("{{name}}")){{else}}{{#if isDiscriminatedObjectList}}_discriminated_list_from_dict(d.get("{{name}}"), cls._{{nestedClassName}}Variants, "{{discriminator}}"){{else}}{{#if isObjectList}}_object_list_from_dict(cls.{{nestedClassName}}, d.get("{{name}}")){{else}}{{#if isDictParam}}_record_from_dict({{/if}}d.get("{{name}}", None){{#if isDictParam}}){{/if}}{{/if}}{{/if}}{{/if}}{{/if}},
3948
{{/each}}
49+
{{#if @root.hasRawJson}}
50+
{{#unless isNested}}
51+
{{memberIndent}} _raw=d,
52+
{{/unless}}
53+
{{/if}}
4054
{{memberIndent}} )

codegen/layouts/resource.hbs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from typing import Any, Dict, List, Literal, Optional, {{#if union.secondaryDiscriminator}}Tuple, {{/if}}Union{{#if union}}, cast{{/if}}
2-
from dataclasses import dataclass
2+
from dataclasses import dataclass{{#if hasRawJson}}, field{{/if}}
3+
{{#if hasRawJson}}
4+
import json
5+
{{/if}}
36
from ..deep_attr_dict import DeepAttrDict
47
from ..parse import (
58
discriminated_list_from_dict as _discriminated_list_from_dict,

codegen/lib/layouts/resources.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818

1919
export interface ResourceLayoutContext {
2020
className: string
21+
hasRawJson?: boolean
2122
moduleName: string
2223
isDeprecated: boolean
2324
deprecationMessage: string
@@ -703,8 +704,10 @@ export const getResourceLayoutContexts = (
703704
const eventModel = blueprint.resources.find(
704705
({ resourceType }) => resourceType === 'event',
705706
)
706-
resources.push(
707-
buildUnionResource(
707+
resources.push({
708+
// raw_json exists for the webhook verify return, so the events carry it and
709+
// nothing else does.
710+
...buildUnionResource(
708711
'SeamEvent',
709712
'event_type',
710713
'seam_event_from_dict',
@@ -718,7 +721,8 @@ export const getResourceLayoutContexts = (
718721
eventModel?.isDeprecated ?? false,
719722
eventModel?.deprecationMessage ?? '',
720723
),
721-
)
724+
hasRawJson: true,
725+
})
722726

723727
const actionAttemptModel = blueprint.resources.find(
724728
({ resourceType }) => resourceType === 'action_attempt',

seam/deep_attr_dict.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import json
2+
3+
14
class DeepAttrDict(dict):
25
"""A dict whose keys are also readable as attributes, nested dicts included.
36
@@ -15,6 +18,10 @@ def __init__(self, value=None):
1518
else:
1619
raise TypeError("expected dict")
1720

21+
def raw_json(self):
22+
"""Return the payload this was parsed from, as JSON."""
23+
return json.dumps(self)
24+
1825
def __setitem__(self, key, value):
1926
if isinstance(value, dict) and not isinstance(value, DeepAttrDict):
2027
value = DeepAttrDict(value)

0 commit comments

Comments
 (0)