Problem
PR #635 widened response-only list fields to Sequence[T], which solved subclass overrides — adopters subclassing Creative with MyCreative can now assign list[MyCreative] to Sequence[Creative] cleanly.
But the more common adopter pattern in salesagent (and likely other production deployments) is cross-class overrides: substituting a shape-compatible but distinct entity class for the canonical one. Sequence[T] doesn't help these because MyDeliveryView is not a subclass of Creative.
Verified cases in salesagent after 5.3.0 bump
Stripped every # type: ignore[assignment] from the schema modules and re-ran mypy against adcp 5.3.0. 12 errors came back. All 12 are cross-class, not subclass.
| File:Line |
Override |
Parent expects |
Cross-class because |
_base.py:874-877 |
list[GeoCountry] (+ 3 siblings) |
Sequence[GeoCountriesExcludeItem] |
GeoCountry is the inclusion variant; spec uses distinct GeoCountriesExcludeItem for exclusion |
_base.py:1322 |
list[Creative] (salesagent) |
Sequence[CreativeAsset] |
Creative extends LibraryCreative, not CreativeAsset |
_base.py:1939 |
list[SignalDeployment] |
Sequence[Deployments | Deployments3] |
SignalDeployment extends PlatformDeployment, not Deployments |
_base.py:2407 |
list[GetMediaBuysMediaBuy] |
Sequence[MediaBuy] |
GetMediaBuysMediaBuy is a delivery-context view, not a MediaBuy subclass |
delivery.py:251 |
list[MediaBuyDeliveryData] |
Sequence[MediaBuyDelivery] |
Different class, same shape |
delivery.py:440 |
list[CreativeDeliveryData] |
Sequence[Creative] |
Different class, same shape |
creative.py:407 |
list[CreativeAsset] (local) |
list[CreativeAsset] (library) |
Same name, different class — local wraps with internal fields |
creative.py:677 |
QuerySummary (local) |
QuerySummary (library) |
Same name, different class |
This is exactly the pattern your own docs at #644 ("Picking the Right Base Class — Context-Specific Schema Variants") explicitly acknowledges as legitimate and documents how to type:ignore correctly. So the SDK has admitted these aren't bugs in adopter code — they're the intended pattern. But the type system has no way to express "this field accepts any class with the shape of X" without giving up on the safety entirely.
Proposed
Pick one. Listed in order of escalating invasiveness.
(a) Document the pattern + provide a typed escape hatch
Smallest change. Ship a SchemaVariant mixin / decorator that marks the override as intentional and silences mypy without # type: ignore. Effectively codifies what PR #644's docs already say.
class GetMediaBuyDeliveryResponse(LibraryGetMediaBuyDeliveryResponse):
media_buy_deliveries: SchemaVariant[list[MediaBuyDeliveryData]]
The type alias resolves to the field type at runtime; mypy treats it as Any-bounded. Loses precise inference inside the override but kills the ignores.
(b) Generate response types as Generic over their entity items
class GetCreativeDeliveryResponse(Generic[CreativeT], ...):
creatives: Sequence[CreativeT]
Then:
class GetCreativeDeliveryResponse(LibraryGetCreativeDeliveryResponse[CreativeDeliveryData]):
... # no override needed; the Generic specialization handles it
Mechanically clean, fully type-safe, but requires the codegen path to emit Generic classes — bigger change.
(c) Use structural Protocol bounds in response types
class CreativeLike(Protocol):
creative_id: str
# ... minimum shape
class GetCreativeDeliveryResponse(...):
creatives: Sequence[CreativeLike]
Any class with the right shape satisfies the type. No subclass relation needed. Closest to what the adopter pattern actually means. But: structural typing in Pydantic-generated types is unusual and requires careful codegen.
What I'd push for
(b) is the cleanest if the codegen pipeline can absorb it. Generic response types specialize cleanly, give adopters full inference inside their overrides, and don't require the SDK to predict which fields adopters will override.
(a) is the smallest reasonable step if (b) is too invasive — it at least removes the visual noise and centralizes the pattern so reviewers know what they're seeing.
Acceptance
Real-world demand
Same audit as #709. This was the single largest category of "workarounds we hoped 5.3.0 would let us delete" that turned out not to be addressed by PR #635.
Problem
PR #635 widened response-only list fields to
Sequence[T], which solved subclass overrides — adopters subclassingCreativewithMyCreativecan now assignlist[MyCreative]toSequence[Creative]cleanly.But the more common adopter pattern in salesagent (and likely other production deployments) is cross-class overrides: substituting a shape-compatible but distinct entity class for the canonical one.
Sequence[T]doesn't help these becauseMyDeliveryViewis not a subclass ofCreative.Verified cases in salesagent after 5.3.0 bump
Stripped every
# type: ignore[assignment]from the schema modules and re-ran mypy against adcp 5.3.0. 12 errors came back. All 12 are cross-class, not subclass._base.py:874-877list[GeoCountry](+ 3 siblings)Sequence[GeoCountriesExcludeItem]GeoCountryis the inclusion variant; spec uses distinctGeoCountriesExcludeItemfor exclusion_base.py:1322list[Creative](salesagent)Sequence[CreativeAsset]CreativeextendsLibraryCreative, notCreativeAsset_base.py:1939list[SignalDeployment]Sequence[Deployments | Deployments3]SignalDeploymentextendsPlatformDeployment, notDeployments_base.py:2407list[GetMediaBuysMediaBuy]Sequence[MediaBuy]GetMediaBuysMediaBuyis a delivery-context view, not aMediaBuysubclassdelivery.py:251list[MediaBuyDeliveryData]Sequence[MediaBuyDelivery]delivery.py:440list[CreativeDeliveryData]Sequence[Creative]creative.py:407list[CreativeAsset](local)list[CreativeAsset](library)creative.py:677QuerySummary(local)QuerySummary(library)This is exactly the pattern your own docs at #644 ("Picking the Right Base Class — Context-Specific Schema Variants") explicitly acknowledges as legitimate and documents how to type:ignore correctly. So the SDK has admitted these aren't bugs in adopter code — they're the intended pattern. But the type system has no way to express "this field accepts any class with the shape of X" without giving up on the safety entirely.
Proposed
Pick one. Listed in order of escalating invasiveness.
(a) Document the pattern + provide a typed escape hatch
Smallest change. Ship a
SchemaVariantmixin / decorator that marks the override as intentional and silences mypy without# type: ignore. Effectively codifies what PR #644's docs already say.The type alias resolves to the field type at runtime; mypy treats it as
Any-bounded. Loses precise inference inside the override but kills the ignores.(b) Generate response types as Generic over their entity items
Then:
Mechanically clean, fully type-safe, but requires the codegen path to emit Generic classes — bigger change.
(c) Use structural Protocol bounds in response types
Any class with the right shape satisfies the type. No subclass relation needed. Closest to what the adopter pattern actually means. But: structural typing in Pydantic-generated types is unusual and requires careful codegen.
What I'd push for
(b) is the cleanest if the codegen pipeline can absorb it. Generic response types specialize cleanly, give adopters full inference inside their overrides, and don't require the SDK to predict which fields adopters will override.
(a) is the smallest reasonable step if (b) is too invasive — it at least removes the visual noise and centralizes the pattern so reviewers know what they're seeing.
Acceptance
# type: ignore[assignment]comments listed above without changing override behavior--strictpasses on the resulting schema modulesexamples/showing the documented adopter patternReal-world demand
Same audit as #709. This was the single largest category of "workarounds we hoped 5.3.0 would let us delete" that turned out not to be addressed by PR #635.