feat: Generate typed nested resource classes - #602
Merged
Conversation
Nested resource objects now hydrate as typed mapping-compatible dataclasses and unknown nested API fields are stripped. Free-form record properties remain mappings. BREAKING CHANGE: Nested properties are typed objects rather than dict subclasses. dict(...) and isinstance(value, dict) no longer work for them; typoed attributes now raise AttributeError instead of returning and inserting an empty mapping; undocumented nested fields are stripped.
Nested dataclasses were emitted at module level with their root resource
name flattened onto the property name, so `device.properties.battery`
became `DeviceBattery`.
Because that name only ever used the root resource name, distinct shapes
at different depths collapsed onto one class. The first shape reached won,
and every other path silently got the wrong fields:
- `device.properties.battery` took the shape of
`device.properties.accessory_keypad.battery`, dropping `status`.
- `device.properties.available_climate_presets[].ecobee_metadata` (and
the `current_` / `default_climate_setting` variants) took the shape of
`device.properties.ecobee_metadata`, dropping `climate_ref`,
`is_optimized`, and `owner`.
- the `phone_session` credential/entrance `assa_abloy_vostio_metadata`
and `visionline_metadata` pairs collapsed onto each other, as did
`device.errors` and `phone_session.errors` against their nested
namesakes.
Emit nested dataclasses inside the class that owns them instead, named
after the property alone, so the enclosing class disambiguates them:
AcsCredential.AssaAbloyVostioMetadata
Device.Properties.Battery
Device.Properties.AccessoryKeypad.Battery
That removes all nine collisions by construction, keeps each resource
module exporting just its resource, and matches the layout the Ruby SDK
generates. `from_dict` reaches nested classes through `cls`, so no
qualified paths appear in the generated bodies.
The generator now builds a class tree rather than a flat map, and raises
if a nested class would shadow a name the module depends on, if two
siblings would claim the same name, or if the shape nests implausibly
deep.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149sGKk8V2fn6dLvzNor86n
`just lint` runs pylint, which exits non-zero on any message. Three
findings predate this branch and fail the Lint job on both Python 3.11 and
3.12:
- `unused-argument` on the generated `from_dict` of a nested class with
no documented properties, where the body is just `return cls()`.
- `no-member` on the test that asserts attribute access raises, which
reads a deliberately absent attribute.
- `use-implicit-booleaness-not-comparison` on `device.errors == []`,
where the point is the exact empty-list value rather than falsiness.
Suppress the first two narrowly and assert the third by type and length.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149sGKk8V2fn6dLvzNor86n
`mergeResourceProperties` kept the first occurrence of each property name
and discarded the rest. That was invisible while merged shapes stayed
`Dict[str, Any]`, but this branch turns them into dataclasses whose
`from_dict` reads field by field, so the dropped fields were discarded
from responses at hydration:
- `ActionAttempt.Result` carried one field, `was_confirmed_by_device`.
An `ENCODE_ACS_CREDENTIAL` attempt's `acs_credential_on_encoder` and
`acs_credential_on_seam`, and a `CREATE_INSTANT_KEY` attempt's
`instant_key_url`, were silently thrown away.
- `AcsUser.PendingMutations.From` carried three of its eight fields, and
the same pattern hit `from`/`to` across ten resources.
Merge recursively and take the union instead, descending into nested
objects and list items so a field survives no matter how deep the variants
disagree. Variants of a nested discriminated list are concatenated and
merged by whoever consumes the list.
The merged dataclass is wider than any single variant, which is the
tradeoff for one class per union: a `LOCK_DOOR` result exposes
`instant_key_url` as None. `main` had the same looseness via DeepAttrDict,
where every key was untyped and possibly absent, so this keeps the
response data intact rather than dropping it.
Merging now raises if variants disagree on a property's shape rather than
silently picking one. The only disagreements in the blueprint today are
`action_attempt.error.type` and `result.errors[].error_code`, both string
against enum, which map to the same Python type; anything structural
fails generation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149sGKk8V2fn6dLvzNor86n
Merging kept the first occurrence's description along with its shape. Each
variant documents a property for its own case, so the surviving text
describes one arm of a union the dataclass no longer distinguishes. Of 271
merged properties, 82 had conflicting descriptions.
Most were merely narrow: `from` on an access code's pending mutations read
"Previous code configuration" on a shape that also covers names. Four were
wrong outright, describing the opposite of what the field means:
:ivar is_device_error: Indicates that the error is not a device error.
Keep a description only when every variant that documents the property
agrees, which also picks the real text when the others are blank. Deprecate
the merged property if any variant deprecates it, so a warning is never
lost to a variant that omits it.
The per-field documentation inside a merged dataclass is unaffected, since
those fields come from a single variant each. Only the summary of a shape
the variants describe differently goes away.
The type packages document each variant correctly and the TypeScript SDK
renders them as a union, so it keeps the precise text. Resolving this
upstream would flatten those descriptions and cost that accuracy, so the
loss stays here, in the SDKs that merge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149sGKk8V2fn6dLvzNor86n
razor-x
marked this pull request as ready for review
August 12, 2026 17:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR refactors the code generation system to create properly typed nested resource classes instead of using generic
Dict[str, Any]for complex nested objects. All nested objects now inherit fromResourceMappingto provide both type safety and backward-compatible dictionary-style access.Key Changes
New
ResourceMappingbase class: Addedseam/utils/resource_mapping.pyto provide a base class for nested resource dataclasses that supports dictionary-style access (__getitem__,__contains__,keys(),values(),items(),get()) for backward compatibility while maintaining type safety.Generated typed nested classes: Updated code generation to create nested
@dataclassclasses for complex properties across all resource types:AcsEntrance: AddedAkilesMetadata,AssaAbloyVostioMetadata, and other provider-specific metadata classesAcsUser: Convertedaccess_schedulefromDict[str, Any]to typedAccessScheduleclassDevice: Significantly expanded with typed nested classes (3156 lines added)AccessCode,AccessGrant,ConnectedAccount,UnmanagedAccessCode,UnmanagedAccessGrant,SeamEvent,UnmanagedDevice,AcsAccessGroup,AcsCredential,Phone,AccessMethod,UnmanagedAccessMethod,AcsSystem, and others: Added typedErrorsand other nested classesCode generation updates: Modified
codegen/lib/layouts/resources.tsandcodegen/layouts/partials/resource-dataclass.hbsto:from_dict()class methods for deserializationResourceMappingfor nested classesType checking: Added mypy configuration to
justfilefor type checking the generated resources with specific error code exclusions.PEP 561 compliance: Added
seam/py.typedmarker file to indicate the package supports type hints.Implementation Details
from_dict()class methods for proper deserialization from API responsesResourceMappingbase class provides transparent dictionary-style access while maintaining type safetyNonedefaultstest/nested_resource_test.pyto verify nested objects are typed and drop unknown fieldshttps://claude.ai/code/session_0149sGKk8V2fn6dLvzNor86n