Mismatch
The Python library type for ImageAsset (and sibling asset variants) declares optional fields as nullable:
```python
class ImageAsset(BaseModel):
asset_type: Literal["image"] = "image"
url: str
width: int
height: int
format: str | None = None
alt_text: str | None = None
provenance: Provenance | None = None
```
The bundled JSON schema (`creative/list-creatives-response.json`, AdCP 3.0.6 +) declares the same fields as non-nullable scalar / object types:
```json
"format": { "type": "string" },
"alt_text": { "type": "string" },
"provenance": { "type": "object", ... }
```
Result: an adopter who happily round-trips the typed model and emits the wire envelope via `model_dump()` includes `format: null` / `alt_text: null` / `provenance: null`. The storyboard SDK's bundled-schema validator rejects this — the asset value matches multiple branches of the AssetVariant `oneOf` (or none), depending on validator semantics.
Repro
Storyboard `media_buy_seller/creative_fate_after_cancellation/list_creatives_before_cancel` against an adopter that returns Pydantic-built `ListCreativesResponse` with an image asset that has no format / alt_text / provenance. Validator fails with `Invalid input` at `/creatives/0/assets/`.
Workaround on the adopter side
Strip null-valued fields from each asset value before `model_dump` runs:
```python
assets_dict = {
key: ({k: v for k, v in value.items() if v is not None or k not in _NULL_STRIP_FIELDS}
if isinstance(value, dict) else value)
for key, value in assets_dict.items()
}
```
bokelley/salesagent does this in `src/core/tools/creatives/listing.py`. Workable but every adopter has to do it.
Suggested fix
Two options:
- Match bundled schema: tighten the Pydantic types to non-nullable. Breaking change for adopters constructing models with explicit `None`s.
- Match Pydantic types: relax the bundled schema to allow null. Spec-level change.
Option 1 is the more conservative read — bundled schemas are the authoritative wire contract for compliance grading. Option 2 widens the wire surface and risks downstream consumers (storyboard validator, third-party buyers) tripping on `null` they don't expect.
If option 1: also flip the model_dump default to `exclude_none=True` on the response wrapper so legacy adopters keep emitting valid wire payloads after the type tightening.
Related
Mismatch
The Python library type for
ImageAsset(and sibling asset variants) declares optional fields as nullable:```python
class ImageAsset(BaseModel):
asset_type: Literal["image"] = "image"
url: str
width: int
height: int
format: str | None = None
alt_text: str | None = None
provenance: Provenance | None = None
```
The bundled JSON schema (`creative/list-creatives-response.json`, AdCP 3.0.6 +) declares the same fields as non-nullable scalar / object types:
```json
"format": { "type": "string" },
"alt_text": { "type": "string" },
"provenance": { "type": "object", ... }
```
Result: an adopter who happily round-trips the typed model and emits the wire envelope via `model_dump()` includes `format: null` / `alt_text: null` / `provenance: null`. The storyboard SDK's bundled-schema validator rejects this — the asset value matches multiple branches of the AssetVariant `oneOf` (or none), depending on validator semantics.
Repro
Storyboard `media_buy_seller/creative_fate_after_cancellation/list_creatives_before_cancel` against an adopter that returns Pydantic-built `ListCreativesResponse` with an image asset that has no format / alt_text / provenance. Validator fails with `Invalid input` at `/creatives/0/assets/`.
Workaround on the adopter side
Strip null-valued fields from each asset value before `model_dump` runs:
```python
assets_dict = {
key: ({k: v for k, v in value.items() if v is not None or k not in _NULL_STRIP_FIELDS}
if isinstance(value, dict) else value)
for key, value in assets_dict.items()
}
```
bokelley/salesagent does this in `src/core/tools/creatives/listing.py`. Workable but every adopter has to do it.
Suggested fix
Two options:
Option 1 is the more conservative read — bundled schemas are the authoritative wire contract for compliance grading. Option 2 widens the wire surface and risks downstream consumers (storyboard validator, third-party buyers) tripping on `null` they don't expect.
If option 1: also flip the model_dump default to `exclude_none=True` on the response wrapper so legacy adopters keep emitting valid wire payloads after the type tightening.
Related
VideoAsset.container_format/video_codec/ etc. andUrlAsset.description.