Skip to content

Generated models drop root-level anyOf/oneOf required groups, so they accept documents their own schema rejects #1361

Closed

Description

@KonstantinMirin

datamodel-code-generator renders a schema root's properties and ignores a root-level
anyOf/oneOf whose arms declare required. The constraint disappears from the generated
class, which then validates documents the schema it came from rejects. 42 generated classes
in the 3.2.1 bundle are affected, including CreateMediaBuyRequest and
SyncCreativesRequest.

Layer: CODEGEN. The schemas encode the constraint correctly and unambiguously. The
generator reads each one — it renders the fields — and emits nothing for the group.

Reproduction

media-buy/create-media-buy-request.json declares five unconditionally required fields and
three ways to be a valid media buy:

root required:  ['idempotency_key', 'account', 'brand', 'start_time', 'end_time']
root anyOf:     [['packages'],
                 ['packages', 'total_budget', 'budget_allocation'],
                 ['proposal_id', 'total_budget']]

Supply the five and nothing from any arm, against adcp==8.0.0:

from adcp.types.generated_poc.media_buy.create_media_buy_request import CreateMediaBuyRequest

print(CreateMediaBuyRequest.__pydantic_decorators__.model_validators)

request = CreateMediaBuyRequest.model_validate(
    {
        "idempotency_key": "idem-key-0123456789",
        "account": {"account_id": "acct_1"},
        "brand": {"brand_id": "brand_1", "domain": "example.com"},
        "start_time": "2026-11-01T00:00:00Z",
        "end_time": "2026-11-30T00:00:00Z",
    }
)
print(request.packages, request.total_budget, request.proposal_id)
{}
None None None

A media buy with no packages, no budget and no proposal. SyncCreativesRequest behaves the
same way, accepting a document carrying none of creatives, assignments or
assignment_operations.

Across the generated tree, 25 of 4534 classes carry a model_validator at all, and each of
those is hand-authored for one schema (_validate_format_reference_xor,
_require_portable_bound). _require_portable_bound is this exact constraint, written out
by hand for the four MediaBuyChangeTermConstraints classes in
scripts/post_generate_fixes.py. The rule behind it covers 42 sites.

Downstream consequence

AdCP 3.1 put creatives in creative/sync-creatives-request.json's root required. AdCP 3.2
moves it into a group alongside assignments and assignment_operations, so a request that
traffics existing creative IDs carries no creatives at all. The generated class expresses
neither version of that rule — creatives is list[Creative] | None = None with no validator —
so an adopter reading it across that spec revision sees nothing change.

The SDK's own public surface shows what filling the gap by hand costs.
adcp.types.SyncCreativesRequest is not the generated class: canonical_creative.py:862
rebuilds it with _canonical_clone under the override
{"creatives": (list[CreativeAsset], Field(min_length=1))}, which carries no default and is
therefore required. Measured against adcp==8.0.0:

public    SyncCreativesRequest.model_validate({}) -> missing=['account', 'creatives', 'idempotency_key']
generated SyncCreativesRequest.model_validate({}) -> missing=['account', 'idempotency_key']

The public class rejects an assignments-only sync that 3.2 permits; the generated class
accepts a sync carrying none of the three groups 3.2 requires. Two faults with opposite
signs on one field, and neither matches the pinned schema. This issue is the second one.

The repo's own fixtures show the same absence. tests/test_feed_mirror.py's make_signal_dict
builds a wholesale signal with neither signal_ref nor signal_id, and
core/wholesale-feed-event.json's $defs/signal is allOf: [core/signal-listing.json], whose
root requires one of the two. The fixture validates today because nothing carries the rule.

What is derivable, and what is not

For anyOf over required groups, "at least one group is present" is the whole constraint.

For oneOf, it is the part the schema states in required. Cardinality is not derivable from
the groups: those arms are distinguished by properties discriminators, and 15 schemas in the
bundle give two or more arms the same required set — core/canonical-media-buy-action.json
gives three arms ['task'], core/registry-event.json gives twenty arms ['event_type']. No
"exactly one required group" reading satisfies those. No two arms are identical in full, so
this is a property of the encoding, not a schema defect.

An arm's not and properties clauses narrow that arm further. Omitting them under-enforces
and never rejects a valid document, because a document satisfying no arm's required set
satisfies no arm.

Fix

Emit a model_validator(mode="after") for the root required groups, derived from the schema,
wherever the root generates one flat model carrying every named field. PR follows.

The canonical clone carries nothing declared on the generated class

_canonical_clone (adcp/types/canonical_creative.py:442) calls create_model with
__base__=(*envelopes, CanonicalBoundaryModel) and the fields _field_definitions copies out
of source.model_fields — annotation and FieldInfo, nothing else. The generated class is not
among the bases:

public    MRO: ['CreateMediaBuyRequest', 'AdcpVersionEnvelope', 'CanonicalBoundaryModel', 'AdCPBaseModel', 'BaseModel', 'object']
generated MRO: ['CreateMediaBuyRequest', 'AdcpVersionEnvelope', 'AdCPBaseModel', 'BaseModel', 'object']
generated class in public MRO: False

The clone is well supplied with validators — it inherits _reject_legacy_creative_identity
from CanonicalBoundaryModel, which the generated class never had. What it cannot carry is
anything declared ON the generated class, because only fields cross. So a validator emitted
for a required group lands on adcp.types.generated_poc.… and not on adcp.types.…:

CreateMediaBuyRequest: public=['_reject_legacy_creative_identity'] generated=['_require_schema_required_group']
SyncCreativesRequest:  public=['_reject_legacy_creative_identity'] generated=['_require_schema_required_group']

Carrying them across needs _canonical_clone to pass the source's model validators, and for
SyncCreativesRequest it also needs the creatives override removed — otherwise the public
class stays stricter than the pin whatever validator it gains. Both are hand-written lines in
one module, separate from the codegen rule, and the PR does not touch them.

Activity

  1. added
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 4, 2026
  2. bokelley commented on Oct 4, 2026

    @bokelley
    Contributor

    Triage

    Classification: Bug
    Bucket(s): validation, codegen (no matching labels found in this repo — gap noted)
    Status: deferred

    What the experts said:

    • Issue is self-contained and thoroughly documented; the root cause (codegen dropping root-level anyOf/oneOf required groups), the 42 affected classes, and the fix strategy (model_validator(mode="after") emitted from schema) are all established by the filing itself.

    My take: PR #1368 (fix(codegen)!: enforce root-level anyOf/oneOf required groups) was opened by @KonstantinMirin minutes after this issue with Closes #1361, comprehensive tests, and an adoption guide. This issue closes on merge. Note that #1368 is a breaking change — 42 generated models will reject previously-accepted invalid documents — appropriate for a bug fix of this kind.

    Blocked-on: #1368 — resurfaces on merge


    Triaged by Claude Code. Session: https://claude.ai/code/session_019vcykrWZuM6tD13utNQog2


    Generated by Claude Code

  3. added
    bugSomething isn't working
    and removed
    claude-triagingTriage routine is actively working on this issue (1-3 min)
    on Oct 4, 2026
  4. added a commit that references this issue on Oct 4, 2026
    2224c78
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions