Repository navigation
feat(adcp/v3): generate Optional[T] for nullable request fields - #605
torbenbrodt wants to merge 1 commit into
Conversation
A request property whose schema allows null means three things on the wire: omitted (keep the stored value), null (clear it) and a value (replace it). A pointer or `any` field decodes null like omission and drops the key on encode, so `update_media_buy` could not clear a cap. The generator now emits adcp.Optional[T], tagged `omitzero` and never `omitempty`, for a property that has a null branch after $ref resolution (a type list with "null", a null anyOf/oneOf branch, a null enum value or const), is not required, and sits in a struct that a `*-request.json` tool schema reaches and no response or webhook schema reaches. T is what the generator emitted for the property without the pointer, so the union fields that fall back to `any` become Optional[any] and the any-coverage gate keeps tracking them with their reviewed reasons. Fields that change type in types_gen.go: - UpdateMediaBuyRequest: daily_budget_cap, budget_cap_timezone, frequency_cap, bidding - PackageUpdate: budget, min_spend_target, daily_budget_cap, bidding - ControlMediaBuyRequest: daily_budget_cap, budget_cap_timezone, frequency_cap, bidding - PackageControl: budget, min_spend_target, daily_budget_cap, bidding The tools/list input schema for update_media_buy is byte-identical before and after. control_media_buy now advertises its five typed properties, where it advertised nothing for the former `any` fields. lint.py checks the same rule: a non-required request property with a null branch must be Optional[...] in Go, or its nulls must sit below a reviewed untyped parent (comply_test_controller params, CanonicalProposal.forecast, and targeting_overlay until it is typed). Plan.ApprovedSellers is not covered yet; Plan stays exempt. This is a source-incompatible change for callers that set or read these fields; MIGRATING.md has the before and after code. An explicit null is now kept on the wire where it was dropped before. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
@torbenbrodt Thanks for the slice. I checked the 16 fields against the pinned 3.2.2 request schemas with my own script and they match. I read the code and did not run the generator, lint or tests.
|
|
@garvitkaushik-123 thanks, all three were right. Two new commits, dd7eb40 and e73d656:
A further review on our side also added migration notes for go-sdk's |
Slice B of the three-state request-field design on #536 (slice A,
Optional[T], merged as #595). #536 closed when 3.3.0-beta.0 merged; that was a side effect of #595's commit footer, not a decision, and B and C are still open.Opens as a draft: it changes the Go type of released fields (escalation category 1 in
AGENTS.md), so it waits for a maintainer's call rather than a review window. The question is under "Breaking" below.What changes
The generator now emits
Optional[T]taggedomitzerofor every non-required request property whose schema has a null branch (typewith"null", ananyOf/oneOfnull branch, or a nullenum/const), in structs reachable from a tool request schema and not shared with any response or webhook schema. Sixteen fields change:UpdateMediaBuyRequestdaily_budget_cap,budget_cap_timezone,frequency_cap,biddingPackageUpdatebudget,min_spend_target,daily_budget_cap,biddingControlMediaBuyRequestdaily_budget_cap,budget_cap_timezone,frequency_cap,biddingPackageControlbudget,min_spend_target,daily_budget_cap,biddingEach of these schemas accepts
null, and for 12 of the 16 it says whatnulldoes: remove a cap or target, restore the default timezone (budget_cap_timezone), or clear a bidding block so the package inherits (biddingonupdate_media_buy). ForPackageControlbudget,min_spend_targetandbidding, andControlMediaBuyRequest.bidding, AdCP 3.2.2 acceptsnullwithout saying what it means; their generated comments say so, andMIGRATING.mdgives the meaning per field. Today a pointer oranyfield decodesnulllike omission, so a seller can't tell the two apart, and re-encoding drops the key.biddingandfrequency_capbecomeOptional[any]and keep their reviewed-any reasons in the coverage gate.tools/list:update_media_buyandcreate_media_buyadvertise byte-identical schemas (a test pinsupdate_media_buyagainst a reflected twin of the old types).control_media_buynow advertises five properties it didn't before, becauseanyfields aren't advertised:daily_budget_cap,budget_cap_timezoneand the packagebudget,daily_budget_cap,min_spend_target, typed["null","number"]or["null","string"]as in the schema; a test pins those five entries and the rest of the schema. Input for those five is now type-checked:"daily_budget_cap":"250"is rejected as invalid input, where the handler used to receive the string.lint.pyreports a non-required nullable request property that isn'tOptionalin a paired hand-written request-only struct (for generated structs the regeneration diff in CI is the guard), and an untyped field hiding nullable properties unless it is on a reviewed list. The list is exactly what lint finds today: test-controllerparams,CanonicalProposal.forecast, andtargeting_overlayonPackageInput,PackageUpdate,PackageControlandProductPurchaseInput(slice C types those). A stale entry fails lint.Plan.approved_sellers(hand-written; fix(adcp/v3): keep an empty approved_sellers list on the wire #594 changes it first) andCreativeInputlocalization(no Go field until feat(adcp/v3): add revision_id and localization to CreativeInput #581).null, a value and a zero value round-trip for all 16 fields;update_media_buyandcontrol_media_buycalls over MCP, throughAddToolandWithIdempotency, reach the handler asIsNull()at media-buy and package level,IsZero()when omitted andSome(0)for a zero; every generatedOptionalfield is taggedomitzeroand neveromitempty; generator and lint tests for the rule, the reachability scan (including webhooks) and the reviewed list.MIGRATING.mdgets a section with all 16 fields at 3.2.1, 3.3.0-beta.0 and now, and before/after code;docs/sdk-typing-policy.mddescribes the rule.Breaking
Source-incompatible for callers of these fields: 7 were released in 3.2.1 (
*float64,*stringorany), and 9 first shipped asanyin 3.3.0-beta.0.adcp.Float64(5)becomesadcp.Some(5.0), andnullis sent withadcp.Null[float64]()(the pointer fields couldn't send it; the beta.0anyfields usedClearField()). A handler asks three questions: present at all (!x.Budget.IsZero(), which replacesx.Budget != nil), which value (x.Budget.Get()), andnull(x.Budget.IsNull()).Get()is false fornull, so an authorization or audit check moved toGet()would let a removal through. Register these types withadcp.AddTool(go-sdk'smcp.AddToolinfersobjectfor anOptionaland rejects every value), and encode withencoding/json(json-iterator ignoresomitzeroand fails withErrOptionalUnset's message).ErrOptionalNilValue's text changed; match it witherrors.Is.MIGRATING.mdhas all of it.The wire changes in two places: an explicit
nullsurvives decode and re-encode, andcontrol_media_buyrejects mistyped values for its five typed controls. The fields and their null branches are already in the pinned 3.2.2 schemas. For the previous minor, AdCP 3.1.1'spackage-update.jsonbudgetis number-only, so anullpackage budget sent to a 3.1 seller is refused by that seller's own validation; the SDK has no version-translation layer to do more yet. There's nofeat!marker becauseRELEASING.mdforbids breaking-change commits underadcp/v3/.@bokelley, should this land on
mainon its own, or would you rather fold it into #602? #602 regeneratestypes_gen.goanyway, so whichever lands second only reruns the generator; say the word and I'll retarget it.reference/seller-agent(pinned to v3.0.0) usesupd.Budget != nilinattemptedActionand when applying updates. When it moves to the in-repo module (check 6 of #564), it must test presence with!IsZero()and treat anullpackage budget (ceiling removed) as an increase or refuse it, not as no action.Checks run locally
generate.pyreproducestypes_gen.go;lint.py --strict;--coverage-max-unreviewed-any 0; Python generator and lint tests;go test -race ./...inadcp/v3on Go 1.26 and 1.27;go vet;golangci-lint.@garvitkaushik-123 @sujanchalla0510, a review is welcome while the ruling is open.
🤖 Generated with Claude Code