Skip to content

A nested AdCP model dumps unset optional fields as null, which 9.0.0 refuses to read back from JSON #1587

Description

@bokelley

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.

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