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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions