Skip to content

fix(types): codegen multi-emission of Creative/Package/etc. breaks Sequence covariant override on required response fields #642

@bokelley

Description

@bokelley

Problem

datamodel-codegen emits subordinate types (`Creative`, `Package`, `MediaBuyDelivery`, etc.) once per response file. The public alias in `adcp.types` resolves to one specific emission, but the response type's field references a different local emission with the same name.

Example:

```python
from adcp.types import Creative

→ adcp.types.generated_poc.creative.get_creative_delivery_response.Creative

from adcp.types.generated_poc.creative.list_creatives_response import (
ListCreativesResponse,
)

ListCreativesResponse.creatives: Sequence[Creative]

↑ this Creative is adcp.types.generated_poc.creative.list_creatives_response.Creative

— a DIFFERENT class with the same name

```

These two `Creative` classes are nominally identical but type-distinct from mypy's perspective. So an adopter pattern:

```python
from adcp.types import Creative

class _Ext(Creative):
extra: str | None = Field(default=None, exclude=True)

class _ExtListResp(ListCreativesResponse):
creatives: list[_Ext] # mypy [assignment] — type-identity mismatch
```

…fails under mypy --strict regardless of #624's Sequence widening, because `_Ext` is a subclass of one `Creative` and the parent expects a different one.

Affected response types (likely incomplete)

Each of these has a field whose element type is locally re-emitted rather than imported from the canonical module:

  • `ListCreativesResponse.creatives` → `Creative`
  • `GetCreativeDeliveryResponse.creatives` → `Creative`
  • `GetMediaBuyDeliveryResponse.media_buy_deliveries` → `MediaBuyDelivery`
  • `GetMediaBuysResponse.media_buys` → `MediaBuy`
  • `GetSignalsResponse.signals` → `Signal`

(These are the same response types where #624's Sequence widening produces incomplete coverage — the widening is necessary but not sufficient because of this codegen-side issue.)

Proposed fix

Two paths:

A) Codegen-side: prefer cross-file import over local re-emission. When datamodel-codegen sees that `Creative` is already defined in a sibling module (e.g. `creative/creative_item.py` or `core/creative.py`), import it rather than re-emit. Same shape, same type identity, override compatibility restored.

B) Post-processor: rewrite all `X` references in a generated file to import from the canonical module. A new function in `scripts/post_generate_fixes.py` that, for each multi-emitted type on an allowlist, replaces local class definitions with `from .canonical_module import X` and keeps the rest of the file pointing at the imported reference.

Approach A is structurally cleaner; approach B is more contained and safer if the canonical-module mapping is unambiguous.

Companion to #624

This issue tracks the type-identity half of the adopter override-pattern friction. #624 (Sequence widening) is required first — without it, even cases where the type identity is correct would still hit list-invariance `[assignment]` errors. After both land, the full Critical Pattern #1 override should typecheck cleanly under mypy --strict with zero `# type: ignore` lines.

How surfaced

While extending the regression test for #624 to cover required-list overrides (`tests/type_checks/extend_response_with_sequence.py`), the override of `ListCreativesResponse.creatives: list[_InternalCreative]` still failed with `[assignment]` even after Sequence widening. Spike traced the failure to type identity, not variance.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions