Repository navigation
A nested AdCP model dumps unset optional fields as null, which 9.0.0 refuses to read back from JSON #1587
Description
Observed
In adcp 9.0.0, an AdCP model leaves its unset optional fields off when it's dumped directly: AdCPBaseModel.model_dump / model_dump_json default to exclude_none=True. When the same model is a field of an ordinary Pydantic model, the parent's serializer never calls that override. The unset fields come out as null, and 9.0.0 (#1555) refuses a JSON null on optional properties whose schema doesn't admit one. So a nested AdCP model's own JSON output fails to validate:
from pydantic import BaseModel
from adcp.types import CpmVendorPricingOption, TrustedMatch
class Parent(BaseModel):
pricing: CpmVendorPricingOption
trusted_match: TrustedMatch
parent = Parent(
pricing=CpmVendorPricingOption(pricing_option_id="cpm", model="cpm", cpm=1.0, currency="USD"),
trusted_match=TrustedMatch(context_match=True),
)
print("direct:", parent.pricing.model_dump_json())
body = parent.model_dump_json()
print("nested:", body)
Parent.model_validate_json(body)On adcp 9.0.0 / Pydantic 2.13 this prints:
direct: {"model":"cpm","cpm":1.0,"currency":"USD","pricing_option_id":"cpm"}
nested: {"pricing":{"model":"cpm","cpm":1.0,"currency":"USD","ext":null,"pricing_option_id":"cpm"},"trusted_match":{"context_match":true,"identity_match":false,"response_types":["activation"],"dynamic_brands":false,"providers":null}}
pydantic_core._pydantic_core.ValidationError: 2 validation errors for Parent
pricing.ext
Input should be an object [type=model_type, input_value=None, input_type=NoneType]
trusted_match.providers
Input should be a valid array [type=list_type, input_value=None, input_type=NoneType]
The same happens one level down: a TrustedMatch with a provider that omits countries / uid_types writes "countries": null, which is then refused. It also happens through a RootModel over an AdCP union.
Expected
An AdCP model serializes the same way wherever it sits, so that model_validate_json(model_dump_json()) round-trips for any parent that holds AdCP models. Before 9.0.0 the nulls were emitted but accepted. Now they're refused, which makes this a serialize/validate inconsistency inside the SDK's own types.
Impact
Any adopter whose HTTP layer validates responses from JSON breaks once it nests AdCP types in its own response models. Spectree, for example, validates bytes with model_validate_json. In the Embedded Sales Agent, the management API returned 500s on signal-mapping pricing options and on any wholesale product with a minimal trusted_match. The current workaround is a per-field WrapSerializer that calls the AdCP model's own model_dump. Every adopter has to find each nesting site and add it.
Suggested fix
Leave unset optional non-nullable fields off at the model level instead of only in the model_dump override. A model_serializer(mode="wrap") on AdCPBaseModel, or on the generated models, that drops None for fields whose schema has no null arm would apply from any parent. Product-style handling in adopter code shows the pattern. It needs to keep the published JSON schema unchanged, so no annotated return type on the serializer.
Found while moving the Embedded Sales Agent from 9.0.0b4 to 9.0.0.
Activity
fix(types): keep wire bytes on parsed URLs, datetimes, UUIDs and emai…
fix(types): inherit the generated field on every hand-written redecla…
fix(types): keep wire bytes on parsed URLs, datetimes, UUIDs and emai…