Skip to content

bug(types): optional const:true properties generate Literal[True] = True defaults, so TargetingOverlaySupport promises every postal system #1347

Closed

Description

@bokelley

Summary

Codegen turns an optional schema property declared as {"const": true} into Literal[True] = True. A model then reports every such field as set to true, including ones the payload never sent, and model_dump(exclude_none=True) emits them.

The worst case is core/positive-postal-area-support.json. Any geo_postal_areas declaration in TargetingOverlaySupport dumps with every deprecated postal flag set. On Product.overlay_support, which AdCP 3.2 defines as a binding promise, that tells buyers the product supports every postal system.

Reproduction (adcp 8.0.0)

from adcp.types._generated import TargetingOverlaySupport

declared = {"geo_postal_areas": {"US": ["zip"]}}
print(TargetingOverlaySupport.model_validate(declared).model_dump(mode="json", exclude_none=True))

Output:

{'geo_postal_areas': {'US': ['zip'], 'us_zip': True, 'us_zip_plus_four': True, 'gb_outward': True,
 'gb_full': True, 'ca_fsa': True, 'ca_full': True, 'de_plz': True, 'fr_code_postal': True,
 'au_postcode': True, 'ch_plz': True, 'at_plz': True}}

Expected: {'geo_postal_areas': {'US': ['zip']}}. exclude_unset=True gives the right answer, but the SDK's own serialization paths (e.g. the MCP/A2A dispatch model_dump(mode="json", exclude_none=True)) don't use it.

Root cause

In core/positive-postal-area-support.json (3.2.1), the second allOf arm narrows the deprecated legacy flags to {"const": true}. Each one is optional; none is in required:

{"type": "object", "minProperties": 1,
 "properties": {"us_zip": {"const": true}, "us_zip_plus_four": {"const": true}, ...}}

The generated model adcp/types/generated_poc/core/positive_postal_area_support.py gives each of them a default of True:

us_zip: Annotated[Literal[True], Field(deprecated=True)] = True
us_zip_plus_four: Annotated[Literal[True], Field(deprecated=True)] = True
...

For an optional property, const restricts the value if present. It doesn't supply a default.

Other places with the same pattern

grep -rn "Literal\[True\].*= True" adcp/types/generated_poc finds 33 fields. Besides the 11 postal flags, there is replayed: Literal[True] = True on response variants in list_products_response.py, request_proposals_response.py, buy_products_response.py, accept_proposal_response.py, control_media_buy_response.py, media_buy_commitment_response.py, refine_proposals_response.py and decline_proposals_response.py. In 3.2.1 list-products-response.json, replayed is {"type": "boolean", "const": true} and is not required. I haven't checked whether union resolution can put a non-replayed response on those variants, but the generated default doesn't match the schema either way.

Suggested fix

For an optional property with const, generate Literal[<const>] | None = None, or leave it required with no default. Only a property listed in required should come out as Literal[...] with no | None. A codegen regression test that flags any = <const> default on a property not in required would catch this class of bug.

Downstream workaround

The embedded sales agent (scope3data/embedded-sales-agent#578) works around it in two places:

  • It stores the seller's overlay_support as schema-validated JSON instead of round-tripping it through the model.
  • get_products re-dumps each product's overlay_support with exclude_unset=True (_with_declared_overlay_support in core/platforms/_delegate.py).

Both can be removed once this is fixed.

Activity

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

    @bokelley
    ContributorAuthor

    Triage

    Classification: Bug
    Bucket(s): validation (no exact matching label — gap noted)
    Status: ready-to-implement

    What the experts said:

    • ad-tech-protocol-expert: Bug confirmed; const: true without required is a restriction on presence, not a default — Literal[True] | None = None is the correct correction per JSON Schema; minor semver bump warranted; watch regex for the compact Annotated[Literal[True], Field(deprecated=True)] form vs. the multi-line canceled/unchanged shape.
    • code-reviewer: Fix location (post_generate_fixes.py) is correct; replayed (22 occurrences across 8 response files) is low-risk and follows the established pattern; PositivePostalAreaSupport postal flags are moderate-risk — the class is explicitly "positive" and may be designed to assert all flags — confirm schema intent before widening; regression test feasible in tests/test_import_layering.py.

    My take: Both experts agree the replayed fix is unambiguous. The postal-flag scope carries a legitimate schema-intent question from the code reviewer, but the issue author (MEMBER) filed this as a bug and the reproduction confirms false capability advertisement on a binding Product.overlay_support field. Treat the postal flags as bug-scope and note the schema-intent question in the PR.


    Implementation brief:

    • Scope A — replayed (22 fields, 8 files). Add fix_replayed_literal_defaults() to scripts/post_generate_fixes.py, modelled on fix_canceled_literal_defaults(). Target: media_buy/{list,request_proposals,buy_products,accept_proposal,control_media_buy,media_buy_commitment,refine_proposals,decline_proposals}_response.py. Widen replayed: Literal[True] = True → Literal[True] | None = None. The replayed field is set by server-originated deserialization, so no caller constructs it bare.

    • Scope B — postal flags (11 fields, core/positive_postal_area_support.py). Add fix_postal_literal_defaults() to post_generate_fixes.py. Handle the compact annotation form: us_zip: Annotated[Literal[True], Field(deprecated=True)] = True → Literal[True] | None = True. Note in PR: PositivePostalAreaSupport semantics should be confirmed — if the "positive" variant is intended to assert all 11 legacy flags unconditionally, the discriminated union should enforce that via required, not defaults. Either way, phantom defaults on an optional field are wrong.

    • Regression test. In tests/test_import_layering.py (or a new tests/test_codegen_invariants.py), parse generated_poc/ with the ast module, collect all field assignments matching = True or = False on classes with a Literal[True]/Literal[False] annotation, and assert the list is empty after post_generate_fixes.py runs. This catches any future const-default misfire.

    • Required checks: pytest tests/ · mypy src/ · ruff check . / black --check . · make validate-generated. No generated_poc/ edits directly — only post_generate_fixes.py changes which take effect on the next make regenerate-schemas.

    • Non-breaking rationale: Callers reading .us_zip (etc.) on a freshly-constructed PositivePostalAreaSupport() will get None instead of True; that code was relying on phantom data. No wire input changes — Literal[True] validation on received values is preserved. Treat as minor-version change per the protocol expert's assessment.

    • Likely files: scripts/post_generate_fixes.py (add two new functions, wire into main()), tests/test_codegen_invariants.py (new regression test). Regenerated generated_poc/ files are also updated but those are auto-generated output.


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


    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 3, 2026
  4. added 2 commits that reference this issue on Oct 3, 2026
    63bfadb
    d4c51d1
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