Summary
Please restrict RootModel to genuine value types (a scalar or a single wrapped value like Country, Gtin, PostalArea) and model object/discriminated-union types as plain BaseModel with a discriminated field instead.
A RootModel wrapper cannot carry an extra-field policy, and pydantic refuses to let a subclass add one:
from pydantic import ConfigDict
from adcp.types.generated_poc.core.pricing_option import PricingOption
class OurPricingOption(PricingOption):
model_config = ConfigDict(extra="forbid")
# pydantic.errors.PydanticUserError:
# `RootModel` does not support setting `model_config['extra']`
Verified against adcp==6.6.0 / spec 3.1.1 on pydantic 2.12.
Why this matters to a consumer
The generated option members carry extra="allow". A seller implementation that wants a stricter policy — ours is forbid in dev/CI and ignore in production, never allow — has no way to apply it to the union as a whole, because the only place to hook is the RootModel and that is exactly where extra is refused.
Two concrete consequences we hit:
-
Non-spec fields silently reach the wire. extra="allow" accepts undeclared attributes, and they serialize. We were emitting two fields on pricing options that do not exist in core/pricing-option.json. additionalProperties is absent from that schema, so draft-07 permits them and no conformance check objects — the only thing that would have caught it is an extra policy we could not set.
-
Legacy field names are accepted indefinitely. With extra="allow", a pre-V3 rate= keyword is accepted and echoed back rather than rejected, so a consumer gets no signal that it should have migrated to fixed_price. Setting forbid on our own subclass surfaced it immediately.
What works today, and what doesn't
The members of the union (CpmPricingOption, VcpmPricingOption, …) are plain BaseModels and subclass cleanly — we can set extra and add internal-only fields with exclude=True on each. So the members are fine; it is only the RootModel wrapper that is unworkable.
Our workaround is to subclass all nine members and rebuild the union ourselves, which means tracking the SDK's shape by hand and re-doing it whenever a member is added. CpaPricingOption and TimeBasedPricingOption already landed after our first pass, which is the maintenance cost this shape imposes.
Requested change
- Keep
RootModel for value types where the wrapped value is the model.
- For discriminated unions and object types, generate a plain
BaseModel carrying the discriminated union as a normal field (e.g. PricingOption.option: Annotated[Cpm | Vcpm | …, Field(discriminator="pricing_model")]), or expose the union as a bare Annotated[...] type alias so consumers can compose it themselves.
Either form lets every consumer apply its own extra policy without re-declaring the SDK's shapes.
Affected types
26 exported RootModels in adcp.types; the ones we consume are AccountReference, Country, GeoCountry, GeoRegion, PostalArea, PropertyTag, PublisherDomain, SignalRef, plus core.pricing_option.PricingOption. Several of those (Country, Gtin, PostalArea) look like legitimate value types; PricingOption and AccountReference are the object/union cases where the wrapper blocks policy.
Summary
Please restrict
RootModelto genuine value types (a scalar or a single wrapped value likeCountry,Gtin,PostalArea) and model object/discriminated-union types as plainBaseModelwith a discriminated field instead.A
RootModelwrapper cannot carry an extra-field policy, and pydantic refuses to let a subclass add one:Verified against
adcp==6.6.0/ spec 3.1.1 on pydantic 2.12.Why this matters to a consumer
The generated option members carry
extra="allow". A seller implementation that wants a stricter policy — ours isforbidin dev/CI andignorein production, neverallow— has no way to apply it to the union as a whole, because the only place to hook is theRootModeland that is exactly whereextrais refused.Two concrete consequences we hit:
Non-spec fields silently reach the wire.
extra="allow"accepts undeclared attributes, and they serialize. We were emitting two fields on pricing options that do not exist incore/pricing-option.json.additionalPropertiesis absent from that schema, so draft-07 permits them and no conformance check objects — the only thing that would have caught it is an extra policy we could not set.Legacy field names are accepted indefinitely. With
extra="allow", a pre-V3rate=keyword is accepted and echoed back rather than rejected, so a consumer gets no signal that it should have migrated tofixed_price. Settingforbidon our own subclass surfaced it immediately.What works today, and what doesn't
The members of the union (
CpmPricingOption,VcpmPricingOption, …) are plainBaseModels and subclass cleanly — we can setextraand add internal-only fields withexclude=Trueon each. So the members are fine; it is only theRootModelwrapper that is unworkable.Our workaround is to subclass all nine members and rebuild the union ourselves, which means tracking the SDK's shape by hand and re-doing it whenever a member is added.
CpaPricingOptionandTimeBasedPricingOptionalready landed after our first pass, which is the maintenance cost this shape imposes.Requested change
RootModelfor value types where the wrapped value is the model.BaseModelcarrying the discriminated union as a normal field (e.g.PricingOption.option: Annotated[Cpm | Vcpm | …, Field(discriminator="pricing_model")]), or expose the union as a bareAnnotated[...]type alias so consumers can compose it themselves.Either form lets every consumer apply its own
extrapolicy without re-declaring the SDK's shapes.Affected types
26 exported
RootModels inadcp.types; the ones we consume areAccountReference,Country,GeoCountry,GeoRegion,PostalArea,PropertyTag,PublisherDomain,SignalRef, pluscore.pricing_option.PricingOption. Several of those (Country,Gtin,PostalArea) look like legitimate value types;PricingOptionandAccountReferenceare the object/union cases where the wrapper blocks policy.