diff --git a/scripts/generate_models_simple.py b/scripts/generate_models_simple.py index 68d9727b..6f6400a9 100755 --- a/scripts/generate_models_simple.py +++ b/scripts/generate_models_simple.py @@ -501,6 +501,7 @@ def add_custom_implementations(code: str) -> str: # The simple code generator produces type aliases (e.g., PreviewCreativeRequest = Any) # for complex schemas that use oneOf. We override them here with proper Pydantic classes # to maintain type safety and enable batch API support. +# Note: All classes inherit from BaseModel (which is aliased to AdCPBaseModel for exclude_none). class FormatId(BaseModel): @@ -723,7 +724,9 @@ def main(): "import re", "from typing import Any, Literal", "", - "from pydantic import BaseModel, ConfigDict, Field, field_validator", + "from pydantic import ConfigDict, Field, field_validator", + "", + "from adcp.types.base import AdCPBaseModel as BaseModel", "", "", "", diff --git a/src/adcp/types/__init__.py b/src/adcp/types/__init__.py index 7638d9d3..30ff4ac0 100644 --- a/src/adcp/types/__init__.py +++ b/src/adcp/types/__init__.py @@ -2,6 +2,7 @@ """Type definitions for AdCP client.""" +from adcp.types.base import AdCPBaseModel from adcp.types.core import ( Activity, ActivityType, @@ -14,6 +15,7 @@ ) __all__ = [ + "AdCPBaseModel", "AgentConfig", "Protocol", "TaskResult", diff --git a/src/adcp/types/base.py b/src/adcp/types/base.py new file mode 100644 index 00000000..ccc28d2f --- /dev/null +++ b/src/adcp/types/base.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +"""Base model for AdCP types with spec-compliant serialization.""" + +from typing import Any + +from pydantic import BaseModel + + +class AdCPBaseModel(BaseModel): + """Base model for AdCP types with spec-compliant serialization. + + AdCP JSON schemas use additionalProperties: false and do not allow null + for optional fields. Therefore, optional fields must be omitted entirely + when not present (not sent as null). + """ + + def model_dump(self, **kwargs: Any) -> dict[str, Any]: + if "exclude_none" not in kwargs: + kwargs["exclude_none"] = True + return super().model_dump(**kwargs) + + def model_dump_json(self, **kwargs: Any) -> str: + if "exclude_none" not in kwargs: + kwargs["exclude_none"] = True + return super().model_dump_json(**kwargs) diff --git a/src/adcp/types/generated.py b/src/adcp/types/generated.py index 8f3af924..eb33e454 100644 --- a/src/adcp/types/generated.py +++ b/src/adcp/types/generated.py @@ -14,7 +14,9 @@ import re from typing import Any, Literal -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import ConfigDict, Field, field_validator + +from adcp.types.base import AdCPBaseModel as BaseModel @@ -945,6 +947,7 @@ class TasksListResponse(BaseModel): # The simple code generator produces type aliases (e.g., PreviewCreativeRequest = Any) # for complex schemas that use oneOf. We override them here with proper Pydantic classes # to maintain type safety and enable batch API support. +# Note: All classes inherit from BaseModel (which is aliased to AdCPBaseModel for exclude_none). class FormatId(BaseModel):