fix: rewrite $ref in top level oneOf/anyOf/allOf branches during variant generation - #83
Open
vishkaty wants to merge 5 commits into
Open
Conversation
(cherry picked from commit 2a9beb9)
(cherry picked from commit 34c3107)
_create_single_variant now rewrites $ref inside top-level oneOf/anyOf/ allOf/items branches (see the preceding two commits, cherry-picked from Universal-Commerce-Protocol#35), which lengthens some array-root item type references, e.g. total.Total becomes total_create_request.TotalCreateRequest for TotalsCreateRequest. That extra length pushes ruff's formatter to wrap Annotated[...] onto multiple lines with a trailing comma before the closing bracket. inject_array_contains spliced AfterValidator(...) in right before that closing bracket without checking for the trailing comma, landing the new element after an orphaned comma with nothing between them - a SyntaxError, not just a formatting nit. Because tests/test_codegen_pipeline.py imported the generated Totals*Request classes inside a bare "except ImportError", the SyntaxError went uncaught and took the whole test module down at collection time, failing every test in the file. Fix the splice to insert after the last real token before the closing bracket instead of blindly before it, reusing an existing trailing comma when present. Also widen the import guard to catch SyntaxError so one broken generated file degrades to HAVE_SDK = False instead of failing collection for the whole module.
Runs generate_models.sh 2026-04-08 (README compat table: SDK 0.4.x -> UCP schema 2026-04-08) against the three preceding commits (the Universal-Commerce-Protocol#35 ref-rewrite cherry-picks plus the inject_array_contains fix). Only totals_create_request.py/totals_update_request.py change beyond what the cherry-picks already carried, because those two needed both fixes together: the ref rewrite (to point at total_create_request.json instead of total.json) and the trailing-comma-safe splice (to still parse once that longer reference pushes Annotated[...] onto multiple lines).
This was referenced Aug 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
What
_create_single_variantinpreprocess_schemas.pybuilds a create/update/complete variant of aschema by filtering
propertiesand rewriting external$reflinks so they point at siblingrequest variants instead of the base response schemas. That rewrite only ran on refs found inside
properties. Schemas whose polymorphism lives at the top level, such as aoneOfunion with noown
properties, or a singleallOfinheritance ref, kept pointing at the base response schemafiles. This change makes the rewrite run over the whole variant tree, so
oneOf,anyOf,allOf,and
itemsare all covered, and it makes the dependency propagation step aware of refs foundoutside
propertiesso the target variant files actually get generated.Observed before this change,
FulfillmentDestinationCreateRequestwrappedShippingDestination | RetailLocation, the response models, whereidis required. A callersubmitting a brand new shipping destination during create, with no
idyet, failed validationthat should have passed.
Expected and now observed,
FulfillmentDestinationCreateRequestwrapsShippingDestinationCreateRequest | RetailLocationCreateRequest, whereidis optional, and thesame payload validates cleanly.
Root cause
preprocess_schemas.py:537to561,_create_single_variant. The function only ever calls_apply_request_rules_to_object(preprocess_schemas.py:508), which readsobject_schema.get("properties", {})(preprocess_schemas.py:512), empty for a schema whose onlycontent is
oneOf,anyOf, or a singleallOfref. No code path walked those branches, sorewrite_refs_to_variants(preprocess_schemas.py:483) never saw the refs inside them.The fix adds one call at the end of
_create_single_variantthat runsrewrite_refs_to_variantsover the entire variant, not onlyproperties, and generalizesextract_external_refsandpropagate_needs_transitiveso a ref found throughoneOf,anyOf,allOf, oritemsis tracked and propagated the same way a property ref already was.The additional postprocess defect
Rewriting these refs lengthens some array root item type references. For
totals.json, the itemtype reference changes from
total.Totaltototal_create_request.TotalCreateRequest. That extralength is enough to push ruff formatting to wrap the surrounding
Annotated[...]onto multiplelines with a trailing comma before the closing bracket.
inject_array_containsinpostprocess_models.py(around line 638 to 674, the function thatthreads an
AfterValidatorinto an array root alias forcontains/minContains/maxContainsenforcement) spliced its new element in right before that closing bracket without checking for an
existing trailing comma. Once the comma was there, the splice landed after it, producing
Field(...),followed by a new line and, AfterValidator(...)], two commas with nothing betweenthem. That is invalid Python, not a formatting complaint, and
ast.parseconfirms it.Because
tests/test_codegen_pipeline.pyimported the generatedTotals*Requestclasses inside abare
except ImportError, andSyntaxErroris not caught by that clause, the broken file took thewhole test module down at collection time. Every test in the file, not only the ones touching
totals, failed to run.
This change fixes the splice in
inject_array_containsto insert after the last real token beforethe closing bracket, reusing an existing trailing comma when present, and widens the import guard
to
except (ImportError, SyntaxError)so one broken generated file degrades to skipping theSDK dependent tests instead of failing collection for the whole module.
Testing
Full suite,
python -m unittest discover -s tests -p "test_*.py", the exact command theschema-preprocessingCI job runs: 88 of 88 tests pass, 0 failures, 0 errors. Main before thischange runs 85 of 85. The 3 added tests are the 2 already present on this branch from the earlier
attempt at this fix, covering
_create_single_variantandpropagate_needs_transitivedirectlyagainst synthetic
oneOf/anyOf/allOfschemas, plus one new test asserting that a line wrappedAnnotated[...]with a trailing comma still parses afterinject_array_containsruns.The original repro now validates cleanly: a create payload for
FulfillmentDestinationCreateRequestwith no
idreturns a populated model instead of raising 3 field required errors.generate_models.sh 2026-04-08, the pinned release per the README compatibility table, was runfrom a clean checkout twice, with the same end of file normalization the
model-driftCI jobapplies. Both runs produced output byte identical to what is committed here, so
model-driftwillstay green.
Kill tests, to confirm each fix is load bearing rather than coincidentally passing: reverting
preprocess_schemas.pyalone, with the tests kept, turns 1 of 88 tests red(
test_composition_variant_rewrites_refs). Revertingpostprocess_models.pyalone, with the testskept, turns a different 1 of 88 red (the new line wrap test), with the same
SyntaxErrorshapethat motivated the fix. Restoring both returns the suite to 88 of 88.
pre-commit runagainst the repo pinned.pre-commit-config.yamlon every hand written file thischange touches (
preprocess_schemas.py,postprocess_models.py,tests/test_codegen_pipeline.py) passes every hook, ruff lint and ruff format run and checkedseparately, with zero files needing modification.
Sweep of the defect class
Every schema in the pinned release with a top level
oneOf,anyOf, orallOfbranch pointing atan external file was checked against its generated output, before and after this change.
fulfillment_destination.jsononeOfmessage.jsononeOfshipping_destination.jsonallOfinheritancetotals.jsonitems.allOfarray root unioncard_credential.jsonallOfinheritancecard_payment_instrument.jsonallOfinheritancedetail_option_value.jsonallOfinheritancetoken_credential.jsonallOfinheritanceNo schema in the release has a property level
oneOf/anyOfwith an external ref left unhandled;that path already worked before this change.
Credit
The preprocessing approach in this change is the one proposed by xiaoxuanz-hub in #35: walk the
whole variant tree for external refs rather than special casing
oneOf/anyOf, and propagatecomposition derived refs the same way property derived refs already propagate. Both commits from
#35 are carried over with their original authorship, along with the test coverage damaz91 added on
that branch. This change supersedes #35 with that approach intact, adding the regeneration against
the pinned spec and a fix for a postprocessing defect that surfaced only once the longer variant
type references from this fix hit
inject_array_contains, which #35 could not have caught sincethe postprocessing constraint enforcement it interacts with landed after #35 was last updated.
Related work
#80, in flight while this change was prepared, adds the same whole tree
rewrite_refs_to_variantscall in
_create_single_variantas part of extension schema support, and generalizesextract_external_refsandpropagate_needs_transitivetoward$defswhere this changegeneralizes them toward the root composition keywords. The two changes fix different generation
gaps and will collide textually in those three functions; whichever lands second needs a small
mechanical rebase, and if #80 goes first I will rebase this change on top of it.
Category (Required)
Please select one or more categories that apply to this change.
ucp-schematool (resolver, linter, validator). (Requires Maintainer approval)Related Issues
Fixes #34. Supersedes #35.
Checklist
!for breaking changes).