Skip to content

Feature Request: Improve Type Ergonomics for Library Consumers #102

@bokelley

Description

@bokelley

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.type expects FormatCategory | None
  • ListCreativeFormatsRequest.asset_types expects list[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.video

Or use Pydantic's BeforeValidator to explicitly handle string->enum coercion.

2. Context fields don't accept dicts

Affected types:

  • ListCreativeFormatsRequest.context expects ContextObject | None
  • GetProductsRequest.context expects ContextObject | 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: ignore

Suggested fix:
Accept dict input that Pydantic coerces:

context: ContextObject | dict[str, Any] | None = None

3. List fields with subclass types cause variance errors

Affected types:

  • PackageRequest.creatives expects list[CreativeAsset] | None
  • CreateMediaBuyRequest.packages expects list[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 = None

Option 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 = None

4. 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))] = None

Priority

From our implementation experience, the impact ranking is:

  1. High: Enum string coercion - affects every request construction
  2. High: Dict->ContextObject coercion - very common pattern
  3. Medium: List variance for extended types - affects advanced implementations
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions