Skip to content

Conversation

@bokelley
Copy link
Contributor

Summary

  • Add flexible input coercion for request types that reduces boilerplate when constructing API requests
  • Enum fields accept string values (e.g., type="video" for FormatCategory.video)
  • List[Enum] fields accept string lists (e.g., asset_types=["image", "video"])
  • Context/Ext fields accept dicts (e.g., context={"key": "value"})
  • FieldModel lists accept strings (e.g., fields=["creative_id", "name"])
  • Document list variance workaround for subclass types

Changes

New files

  • src/adcp/types/coercion.py - Validator utilities and documentation for type coercion
  • src/adcp/types/_ergonomic.py - Applies coercion validators to generated types at import time
  • tests/test_type_coercion.py - Comprehensive tests for all coercion behavior (32 tests)

Modified files

  • src/adcp/types/__init__.py - Import _ergonomic module to apply coercion, add docstring examples

Affected Types

  • ListCreativeFormatsRequest (type, asset_types, context, ext)
  • ListCreativesRequest (fields, context, ext, sort)
  • GetProductsRequest (context, ext)
  • PackageRequest (ext)
  • Sort (field, direction)

Usage Examples

# Before (verbose)
from adcp import ListCreativeFormatsRequest, FormatCategory, ContextObject

req = ListCreativeFormatsRequest(
    type=FormatCategory.video,
    asset_types=[AssetContentType.image, AssetContentType.video],
    context=ContextObject(user_id="123")
)

# After (ergonomic - same result)
req = ListCreativeFormatsRequest(
    type="video",
    asset_types=["image", "video"],
    context={"user_id": "123"}
)

Backward Compatibility

All changes are backward compatible. Existing code using explicit enum values and model instances continues to work unchanged.

Test plan

  • All 32 new coercion tests pass
  • All 419 existing tests pass
  • Linting passes

Closes #102

🤖 Generated with Claude Code

@bokelley
Copy link
Contributor Author

Great PR! The enum coercion and dict-to-model coercion will significantly improve the developer experience.

Regarding list variance - the documentation is helpful, but I have one additional suggestion that could eliminate most cast() calls in practice:

Suggestion: Add list item coercion for fields with base types

Similar to how enums are coerced, you could add a BeforeValidator that coerces list items:

def coerce_list_items(base_class: type[M]) -> Callable[[Any], list[M] | None]:
    """Coerce list items to base class instances.
    
    This handles the list variance issue by accepting any iterable of items
    that are instances of (or can be coerced to) the base class.
    """
    def validator(v: Any) -> list[M] | None:
        if v is None:
            return None
        if not isinstance(v, (list, tuple)):
            return v  # Let Pydantic handle the error
        
        result = []
        for item in v:
            if isinstance(item, base_class):
                # Already correct type (or subclass) - keep as-is
                result.append(item)
            elif isinstance(item, dict):
                # Dict coercion
                result.append(base_class(**item))
            else:
                result.append(item)  # Let Pydantic validate
        return result
    return validator

Then apply to PackageRequest.creatives:

_patch_field_annotation(
    PackageRequest,
    "creatives",
    Annotated[
        list[CreativeAsset] | None,
        BeforeValidator(coerce_list_items(CreativeAsset))
    ],
)

This approach:

  1. Runtime: Works today - subclass instances are valid CreativeAsset instances
  2. Type checking: The validator accepts list[Any] at input, so mypy won't complain about list[MyCreative]
  3. Backward compatible: Existing code passing list[CreativeAsset] still works

The key insight is that the validator's input type is Any, which satisfies mypy, while the output is list[CreativeAsset], which satisfies the field's expected type.

This wouldn't work for all use cases (e.g., function signatures in user code), but it would eliminate the most common pain point: constructing request objects with extended types.


Alternatively, if you want to keep the current approach, the documentation is solid. One minor addition might be a concrete example in the PackageRequest docstring:

class PackageRequest(BaseModel):
    """...
    
    Note: If using extended CreativeAsset subclasses, use cast():
        creatives = cast(list[CreativeAsset], my_extended_creatives)
        PackageRequest(creatives=creatives, ...)
    """

Either way, this PR is a big improvement! 🎉

@bokelley bokelley force-pushed the bokelley/fix-tool-list-api branch from a1e9f70 to ac554d8 Compare December 21, 2025 02:00
Add flexible input coercion for request types that reduces boilerplate
when constructing API requests. All changes are backward compatible.

Improvements:
- Enum fields accept string values (e.g., type="video")
- List[Enum] fields accept string lists (e.g., asset_types=["image", "video"])
- Context/Ext fields accept dicts (e.g., context={"key": "value"})
- FieldModel lists accept strings (e.g., fields=["creative_id", "name"])
- Sort fields accept string enums (e.g., field="name", direction="asc")
- Subclass lists accepted without cast() for all major list fields

Affected types:
- ListCreativeFormatsRequest (type, asset_types, context, ext)
- ListCreativesRequest (fields, context, ext, sort)
- GetProductsRequest (context, ext)
- PackageRequest (creatives, ext)
- CreateMediaBuyRequest (packages, context, ext)
- UpdateMediaBuyRequest.Packages (creatives, creative_assignments)

The list variance issue is now fully resolved - users can pass list[Subclass]
where list[BaseClass] is expected without needing cast().

Closes #102

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@bokelley bokelley force-pushed the bokelley/fix-tool-list-api branch from ac554d8 to d9f1a65 Compare December 21, 2025 02:03
bokelley and others added 2 commits December 20, 2025 21:25
Replace manually-maintained _ergonomic.py with auto-generated version.
The new script introspects Pydantic models at runtime to detect fields
needing coercion, eliminating manual maintenance when schemas change.

Changes:
- Add scripts/generate_ergonomic_coercion.py for auto-generation
- Integrate into generate_types.py pipeline
- Add rationale comment explaining import-time patching choice
- Remove unnecessary hasattr check in _patch_field_annotation
- Discover additional coercion opportunities (pacing enum, packages list)

The generator detects:
- Enum fields -> string coercion
- list[Enum] fields -> string list coercion
- ContextObject/ExtensionObject -> dict coercion
- list[BaseModel] fields -> subclass list coercion

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Different Python/mypy versions report different error codes for the
same type issues (return-value vs no-any-return). Using plain
`# type: ignore` comments avoids unused-ignore errors across versions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@bokelley bokelley merged commit 75dec68 into main Dec 21, 2025
8 checks passed
bokelley added a commit that referenced this pull request Dec 21, 2025
Apply the same BeforeValidator coercion pattern from PR #103 to response
types. This eliminates the need for cast() calls when constructing
response objects with subclass instances or dict coercion.

Response types now support:
- Dict coercion for context/ext fields
- Subclass list acceptance for products, creatives, formats, packages,
  errors, and media_buy_deliveries

Closes #105

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
bokelley added a commit that referenced this pull request Dec 21, 2025
* feat: extend type ergonomics to response types

Apply the same BeforeValidator coercion pattern from PR #103 to response
types. This eliminates the need for cast() calls when constructing
response objects with subclass instances or dict coercion.

Response types now support:
- Dict coercion for context/ext fields
- Subclass list acceptance for products, creatives, formats, packages,
  errors, and media_buy_deliveries

Closes #105

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: improve test imports and documentation

- Update response type tests to import from public API (adcp.types)
  instead of internal generated_poc modules
- Use semantic alias CreateMediaBuySuccessResponse instead of
  CreateMediaBuyResponse1 for clearer intent
- Add clarifying comments for # type: ignore annotations explaining
  Python list covariance limitation
- Add tests for GetProductsResponse.products subclass acceptance
- Add tests for errors field coercion
- Document _ergonomic.py in CLAUDE.md import architecture section

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Improve Type Ergonomics for Library Consumers

2 participants