Repository navigation
fix(types): inherit the generated constraints on the canonical creatives field - #1548
Conversation
…ves field The canonical SyncCreativesRequest redeclares `creatives` to narrow the element type to CreativeAsset, and assigned a fresh Field() to do it. A fresh FieldInfo replaces the inherited one wholesale, so the generated field's max_length=100 and the bundle's description were lost: measured against the pinned 3.2 schema, the model published a node with neither maxItems nor description, and accepted 101 creatives where the schema declares maxItems: 100. That drop predates adcontextprotocol#1546 -- the line read `Field(min_length=1)` before it, with the same two pieces missing. `_inherit` in the same module exists for this case; its docstring warns that a bare Field() "would silently drop min_length and friends". The redeclaration now spells only the annotation it narrows and inherits everything else. What this does NOT do: it does not refuse an explicit JSON null for `creatives`. The generated parent renders the optional property as `list[Creative] | None`, so a null is admitted before any canonical redeclaration is consulted -- deleting the redeclaration outright leaves it accepted. That half is a generator-level rule, filed separately as adcontextprotocol#1547. Two tests: one grades the published node against the bundled node on type, description, minItems and maxItems; the other asserts the ceiling is enforced rather than advertised, on both the model and the JSON-Schema arm.
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean fix with no blocking or medium findings.
Checked: the diff swaps a hand-written Field(default=None, min_length=1) on canonical SyncCreativesRequest.creatives for _inherit(...), restoring the max_length=100 ceiling and description from the generated parent FieldInfo that the hand-written Field had silently dropped. This closes a real divergence where the model accepted 101 creatives while validate_request rejected them. default=None (preserving #1546's optional behavior) and the CreativeAsset element narrowing are preserved. Two added tests grade the published node against the bundle and assert the ceiling is enforced on both arms. fix: is the correct semver signal — no public-surface break. No forward-compat, import-layering, or generated-code concerns.
Decision table: no critical/high (row 1 n/a); gated_paths false (row 2 n/a); high_risk false (rows 3/5 n/a); no medium findings (rows 4/8 n/a); no prior escalation (row 6 n/a); no no-auto-approve team match (row 7 n/a). Falls through to row 9 → approve.
What this changes
The canonical
SyncCreativesRequestredeclarescreativesfor one reason — to narrow the elementtype from the generated
Creativeto the canonicalCreativeAsset— and it did that by assigning afresh
Field(...). A freshFieldInforeplaces the inherited one wholesale, so the generatedfield's
max_length=100and the bundle's description went with it.This swaps the assignment for
_inherit, the helper already in that module for exactly this case.Its docstring states the rule:
What was wrong, measured
On
mainat5ce9ee4dc(CPython 3.13.11), the pinned 3.2 bundle declarescreatives: {type: array, minItems: 1, maxItems: 100, description: "Array of creative assets to sync (create or update)"}. The canonical model published:No
maxItems, nodescription. And the ceiling was not merely unadvertised — a sync of 101creatives was accepted by the model, while
validate_request(...)reported('/creatives', 'maxItems'). The two arms disagreed.This drop predates #1546: before that merge (
15c85cf6e) the line readcreatives: list[CreativeAsset] = Field(min_length=1), and 101 creatives were accepted there toowith the same two pieces already missing from the published node.
What this does not fix
It does not refuse an explicit JSON
nullforcreatives. The generated parent renders theoptional property as
list[Creative] | None, so anullis admitted before any canonicalredeclaration is consulted — deleting the redeclaration outright leaves the
nullaccepted. Thathalf is a generator-level rule affecting thousands of properties and is filed separately as
#1547, which also records why neither deleting the redeclaration nor giving it a non-nullable
annotation is the right fix. Not marking this PR as closing it.
Context: #1545, #1546.
Tests
Two added to
tests/test_sync_creatives_assignment_only.py:test_public_schema_keeps_the_bundled_constraints_on_creatives— grades the published nodeagainst the bundled node on
type,description,minItemsandmaxItems. The file's existingrequiredtest does not catch this: a replacedFieldInfodrops the constraints while leavingrequiredexactly as that test wants it.test_sync_refuses_more_creatives_than_the_bundle_permits— asserts the ceiling is enforced,not advertised, reading
maxItemsout of the bundle rather than hardcoding 100, and checking boththe model (
too_longat('creatives',)) and the JSON-Schema arm.tests/test_sync_creatives_assignment_only.pyis 20 passed on this branch (CPython 3.13.11),including #1546's own assignment-only and canonical-parse tests.
Suggested generator-level treatment for the remaining half
Render an optional-non-nullable property as absent-or-valid rather than
T | None: accept theproperty being missing, refuse an explicit
null. A property whose schema node does list"null"keeps a nullable annotation. Detail and measurements in #1547.