diff --git a/src/adcp/types/canonical_creative.py b/src/adcp/types/canonical_creative.py index 8e3fe787a..de4ebf000 100644 --- a/src/adcp/types/canonical_creative.py +++ b/src/adcp/types/canonical_creative.py @@ -690,6 +690,13 @@ def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: Binding happens AFTER class creation, so pydantic never considers the name a field candidate; ``model_fields``, the JSON schema and the wire are all unaffected. Measured scope: one such validator, on one model. + + + The rebuild does not raise on an unresolved forward reference. A + subclass may annotate a field with a model defined later in its module + (``creatives: list["CreativeAsset"]``); plain pydantic defers such a + class until ``model_rebuild()``, and a raising rebuild here would turn + that ordinary pattern into an error at class creation. """ removed = [name for name in cls.model_fields if is_legacy_creative_identity_key(name)] @@ -698,7 +705,7 @@ def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: cls.__annotations__.pop(name, None) setattr(cls, name, None) if removed: - cls.model_rebuild(force=True) + cls.model_rebuild(force=True, raise_errors=False) @model_validator(mode="before") @classmethod diff --git a/tests/test_canonical_removed_fields.py b/tests/test_canonical_removed_fields.py index d0ad661b8..3f3d46f70 100644 --- a/tests/test_canonical_removed_fields.py +++ b/tests/test_canonical_removed_fields.py @@ -387,3 +387,25 @@ def on(line: int) -> list[str]: f"{checker} does not know {name} on the wire model {class_name} either, " f"so the refusal above proves nothing: {on(line)}" ) + + +def test_a_subclass_may_forward_reference_a_later_model() -> None: + """The removal's rebuild defers an unresolved forward reference, as pydantic does. + + A subclass that re-inherits a removed field triggers the forced rebuild; if + that rebuild raises, annotating a field with a model defined further down the + module fails at class creation instead of at the caller's ``model_rebuild()``. + """ + from adcp.types import PackageRequest + + class WithForwardRef(PackageRequest): + later: list[Later] | None = None + + class Later(BaseModel): + name: str + + assert not WithForwardRef.__pydantic_complete__ + WithForwardRef.model_rebuild() + + assert "later" in WithForwardRef.model_fields + assert not set(_removed_names(WithForwardRef)) & set(WithForwardRef.model_fields)