You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Consumers that subclass library types (the documented extension pattern) cannot upgrade past 3.6.0 because several key types became RootModel discriminated unions.
Pydantic 2 hard-rejects model_config['extra'] on any RootModel subclass:
fromadcp.typesimportGetProductsRequestasLibraryGetProductsRequestclassGetProductsRequest(LibraryGetProductsRequest):
model_config=ConfigDict(extra="forbid") # dev/CI strictness# ...# PydanticUserError: `RootModel` does not support setting `model_config['extra']`
This isn't a hypothetical — prebid/salesagent subclasses 28 library types with environment-specific extra policy ("forbid" in dev/CI, "ignore" in production). We're stuck on 3.6.0 because 3.7+ turns GetProductsRequest, GetSignalsRequest, FrequencyCap, and others into RootModel wrappers that can't be extended.
Root cause chain
JSON Schema uses oneOf for variant types (e.g., get-products-request.json has three buying_mode variants: brief, wholesale, refine)
datamodel-codegen faithfully translates oneOf → RootModel[Variant1 | Variant2 | ...] — this is correct generator behavior
Pydantic 2 forbids model_config overrides on RootModel subclasses — RootModel has no fields of its own, so extra is meaningless on the wrapper
Consumers who subclass these types to add model_config, internal fields, or validators hit a hard wall
The __getattr__ proxy from #150 solves the attribute access ergonomics beautifully, but doesn't address the subclassing constraint — these are separate problems.
Proposed fix: selective unwrap in post_generate_fixes.py
For types that consumers need to subclass (request/response types), replace the RootModel wrapper with a plain Union type alias as a post-generation step:
classGetProductsRequest(LibraryGetProductsRequest1):
model_config=ConfigDict(extra="forbid")
# works — GetProductsRequest1 is a regular AdCPBaseModel
Which types to unwrap
Not all 69 RootModels — only the ones that appear in consumer type hierarchies. Value-type RootModels (PricingOption, Destination, AccountReference) are leaf types that nobody subclasses; they keep the RootModel wrapper + __getattr__ proxy.
Proposed initial set:
Type
Why consumers subclass it
GetProductsRequest
Add model_config, internal fields like product_selectors
GetSignalsRequest
Add model_config for environment-based validation
FrequencyCap
Add custom fields (scope), override validation
CreateMediaBuyResponse
Add model_config for forward compatibility
This list would be a constant in post_generate_fixes.py (like _UNWRAP_TO_UNION) — easy to extend as more types are reported.
Why this approach
Fits the existing pattern — post_generate_fixes.py already does 7 post-generation corrections (forward refs, deprecated fields, discriminators, __getattr__ proxy). This is one more.
Survives regeneration — runs after every generate_types.py invocation.
Non-breaking — isinstance(req, GetProductsRequest1) works the same. Consumers who don't subclass see no difference. The __getattr__ proxy stays for value-type RootModels.
prebid/salesagent attempted a full upgrade to 3.8.0 and had to revert after hitting RootModel subclassing failures across GetProductsRequest, GetSignalsRequest, FrequencyCap, CreateMediaBuyRequest (which gained a required account field on a RootModel parent), and SyncCreativesRequest. The revert touched 15 files. We'd like to get this resolved for the next release so we can cleanly adopt 3.8+.
Happy to submit a PR with the post_generate_fixes.py implementation if this approach works.
Problem
Consumers that subclass library types (the documented extension pattern) cannot upgrade past 3.6.0 because several key types became
RootModeldiscriminated unions.Pydantic 2 hard-rejects
model_config['extra']on anyRootModelsubclass:This isn't a hypothetical — prebid/salesagent subclasses 28 library types with environment-specific
extrapolicy ("forbid"in dev/CI,"ignore"in production). We're stuck on 3.6.0 because 3.7+ turnsGetProductsRequest,GetSignalsRequest,FrequencyCap, and others intoRootModelwrappers that can't be extended.Root cause chain
oneOffor variant types (e.g.,get-products-request.jsonhas threebuying_modevariants: brief, wholesale, refine)oneOf→RootModel[Variant1 | Variant2 | ...]— this is correct generator behaviormodel_configoverrides onRootModelsubclasses —RootModelhas no fields of its own, soextrais meaningless on the wrappermodel_config, internal fields, or validators hit a hard wallThe
__getattr__proxy from #150 solves the attribute access ergonomics beautifully, but doesn't address the subclassing constraint — these are separate problems.Proposed fix: selective unwrap in
post_generate_fixes.pyFor types that consumers need to subclass (request/response types), replace the
RootModelwrapper with a plainUniontype alias as a post-generation step:Before (generated):
After (post-generation fix):
Consumers then subclass the variant they need:
Which types to unwrap
Not all 69 RootModels — only the ones that appear in consumer type hierarchies. Value-type RootModels (
PricingOption,Destination,AccountReference) are leaf types that nobody subclasses; they keep theRootModelwrapper +__getattr__proxy.Proposed initial set:
GetProductsRequestmodel_config, internal fields likeproduct_selectorsGetSignalsRequestmodel_configfor environment-based validationFrequencyCapscope), override validationCreateMediaBuyResponsemodel_configfor forward compatibilityThis list would be a constant in
post_generate_fixes.py(like_UNWRAP_TO_UNION) — easy to extend as more types are reported.Why this approach
post_generate_fixes.pyalready does 7 post-generation corrections (forward refs, deprecated fields, discriminators,__getattr__proxy). This is one more.generate_types.pyinvocation.isinstance(req, GetProductsRequest1)works the same. Consumers who don't subclass see no difference. The__getattr__proxy stays for value-type RootModels.--allow-extra-fields(the proposed fix in Support consumer-configurable extra policy on AdCPBaseModel #153) is the right fix for the blanketextra='allow'problem, but it doesn't solve the "can't subclass RootModel" problem. Both fixes are needed independently.Impact
prebid/salesagent attempted a full upgrade to 3.8.0 and had to revert after hitting
RootModelsubclassing failures acrossGetProductsRequest,GetSignalsRequest,FrequencyCap,CreateMediaBuyRequest(which gained a requiredaccountfield on aRootModelparent), andSyncCreativesRequest. The revert touched 15 files. We'd like to get this resolved for the next release so we can cleanly adopt 3.8+.Happy to submit a PR with the
post_generate_fixes.pyimplementation if this approach works.