-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Feature Request: Improve Type Ergonomics for Library Consumers
Summary
When implementing an AdCP-compliant sales agent using the adcp library (v2.14.0), we encounter several type friction points that require workarounds like cast(), getattr(), or # type: ignore comments. These stem from strict typing that's great for serialization but makes construction verbose.
This issue proposes small, backward-compatible changes to improve developer experience while maintaining type safety.
Problem Areas
1. Enum fields don't accept string values
Affected types:
ListCreativeFormatsRequest.typeexpectsFormatCategory | NoneListCreativeFormatsRequest.asset_typesexpectslist[AssetContentType] | None
Current behavior:
from adcp import ListCreativeFormatsRequest
from adcp.types.generated_poc.enums.format_category import FormatCategory
# Works but verbose
req = ListCreativeFormatsRequest(type=FormatCategory.video)
# Fails type checking (but works at runtime due to Pydantic coercion)
req = ListCreativeFormatsRequest(type="video") # type: ignore[arg-type]Suggested fix:
Update field annotations to accept strings:
type: FormatCategory | str | None = None # Pydantic will coerce "video" -> FormatCategory.videoOr use Pydantic's BeforeValidator to explicitly handle string->enum coercion.
2. Context fields don't accept dicts
Affected types:
ListCreativeFormatsRequest.contextexpectsContextObject | NoneGetProductsRequest.contextexpectsContextObject | None- Various response types with context fields
Current behavior:
# Requires explicit conversion
from adcp.types.generated_poc.core.context import ContextObject
req = ListCreativeFormatsRequest(
context=ContextObject(**{"key": "value"}) # Verbose
)
# Or use type: ignore
req = ListCreativeFormatsRequest(context={"key": "value"}) # type: ignoreSuggested fix:
Accept dict input that Pydantic coerces:
context: ContextObject | dict[str, Any] | None = None3. List fields with subclass types cause variance errors
Affected types:
PackageRequest.creativesexpectslist[CreativeAsset] | NoneCreateMediaBuyRequest.packagesexpectslist[PackageRequest]- Various response types with list fields
Problem:
Implementations often extend library types with internal fields:
from adcp.types.generated_poc.core.creative_asset import CreativeAsset
class Creative(CreativeAsset):
"""Extended with internal tracking fields."""
tenant_id: str | None = Field(None, exclude=True)
platform_creative_id: str | None = Field(None, exclude=True)Due to Python's list invariance, passing list[Creative] where list[CreativeAsset] is expected fails type checking:
# Fails mypy even though Creative is a subclass of CreativeAsset
packages = [PackageRequest(creatives=[Creative(...)])] # type: ignore[arg-type]Suggested fixes:
Option A: Use Sequence (covariant) instead of list for input types:
from collections.abc import Sequence
creatives: Sequence[CreativeAsset] | None = NoneOption B: Document the extension pattern and suggest cast() as the canonical workaround.
Option C: Use TypeVar with bounds:
from typing import TypeVar
T = TypeVar('T', bound=CreativeAsset)
creatives: list[T] | None = None4. ListCreativesRequest.fields expects FieldModel objects
Current behavior:
from adcp import ListCreativesRequest
# User wants to specify fields as strings (natural API)
req = ListCreativesRequest(fields=["creative_id", "name", "format_id"]) # type: ignore
# Required verbose construction
req = ListCreativesRequest(fields=[
FieldModel(field="creative_id"),
FieldModel(field="name"),
FieldModel(field="format_id"),
])Suggested fix:
Accept string list with coercion:
fields: list[FieldModel | str] | None = None
@field_validator('fields', mode='before')
def coerce_fields(cls, v):
if v is None:
return None
return [FieldModel(field=f) if isinstance(f, str) else f for f in v]Implementation Suggestion
These changes can be implemented using Pydantic's validation features without breaking existing code:
from pydantic import BeforeValidator, field_validator
from typing import Annotated
def coerce_enum(enum_class):
def validator(v):
if isinstance(v, str):
return enum_class(v)
return v
return validator
# Example usage
class ListCreativeFormatsRequest(BaseModel):
type: Annotated[FormatCategory | None, BeforeValidator(coerce_enum(FormatCategory))] = NonePriority
From our implementation experience, the impact ranking is:
- High: Enum string coercion - affects every request construction
- High: Dict->ContextObject coercion - very common pattern
- Medium: List variance for extended types - affects advanced implementations
- Low: FieldModel string coercion - less commonly used
Backward Compatibility
All suggested changes are backward compatible:
- Existing code using enum values continues to work
- Existing code using typed objects continues to work
- New code can use simpler string/dict forms
Environment
- adcp version: 2.14.0
- Python: 3.12
- Pydantic: 2.x
- mypy: latest
Related
This issue was identified while implementing the AdCP reference sales agent. The workarounds are functional but add unnecessary complexity and reduce type safety benefits.