Skip to content

fix(workflow): normalize unordered set keywords in schema comparison - #1138

Merged
jjsasha63 merged 3 commits into
google:mainfrom
winklemad:fix-schema-required-order
Sep 1, 2026
Merged

jjsasha63 merged 3 commits into
google:mainfrom
winklemad:fix-schema-required-order

Conversation

@winklemad

Copy link
Copy Markdown
Contributor

Summary

Static schema validation (validateStaticSchemas) rejects valid workflow edges when two structurally identical schemas declare their fields in a different order. workflow.New fails with schema mismatch on edge A -> B for a perfectly compatible edge.

No linked issue — found while reviewing the schema-validation path.

Root cause

For each typed edge, validateStaticSchemas canonicalizes the upstream OutputSchema() and the downstream InputSchema() with utils.CanonicalSchemaJSON and compares the bytes. CanonicalSchemaJSON sorts object keys but leaves arrays in source order, so required is compared order-sensitively. jsonschema.For builds required in struct-field declaration order, so equivalent schemas whose fields are declared in a different order canonicalize differently and fail validation.

type Out struct { Foo string `json:"foo"`; Bar int `json:"bar"` } // required: ["foo","bar"]
type In  struct { Bar int `json:"bar"`;  Foo string `json:"foo"` } // required: ["bar","foo"]
// edge Out -> In fails: graph validation failed: schema mismatch on edge A -> B

Fix

Sort the unordered set-valued keywords (required and a type union) when canonicalizing, so equivalent schemas compare equal. Arrays for other keywords keep their order, since keywords like prefixItems are order-significant. CanonicalSchemaJSON is only consumed by schemasEqualCanonical for equality checks, so nothing else is affected.

Tests

  • internal/utils: TestCanonicalSchemaJSON_RequiredOrder and TestCanonicalSchemaJSON_TypeUnionOrder — reordered required / type unions now canonicalize identically.
  • workflow: added a differ only in required order -> success case to TestStaticSchemaValidation, next to the existing PropertyOrder case. It fails on main with schema mismatch on edge A -> B and passes with this change.

Verified locally with the CI command go test -race -mod=readonly -count=1 -shuffle=on ./... (all packages pass), plus go vet, gofmt, and golangci-lint run clean.

winklemad and others added 3 commits July 9, 2026 20:09
Static schema validation compares an edge's upstream output schema against
the downstream input schema by canonicalizing both and testing byte equality.
CanonicalSchemaJSON sorted object keys but kept arrays in source order, so
`required` was compared order-sensitively. jsonschema.For builds `required`
in struct-field declaration order, so two otherwise identical schemas whose
fields are declared in a different order canonicalized differently and were
rejected with "schema mismatch on edge ...".

Sort the unordered set-valued keywords (`required` and a `type` union) when
canonicalizing so equivalent schemas compare equal. Arrays for other keywords
keep their order, since keywords such as prefixItems are order-significant.

@jjsasha63 jjsasha63 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — I reproduced the bug on main and confirmed the fix works, including for nested structs. All three new tests fail if I swap in the pre-fix schema_canonical.go, so they are testing the right thing.

Two notes, neither of which I think needs to block.

1. enum is left out. An enum array is an unordered set too (JSON Schema 2020-12 §6.1.2), so the same mismatch survives:

{"enum":["x","y"],"type":"string"}  !=  {"enum":["y","x"],"type":"string"}

dependentRequired values are sets of property names and are also unsorted. Since the title says "unordered set keywords" in general, could you either add enum with a test, or narrow the title and the doc comment to say only required and a type union are handled?

2. The sort keys off the name alone, so it also reaches data. unorderedArrayKeywords[k] is checked against a plain map[string]any walk, so any key named required or type holding an array gets sorted, including inside default, const and examples, where the order is real data:

Default {"type":["email","sms"]}  and  Default {"type":["sms","email"]}
  both canonicalize to {"default":{"type":["email","sms"]},"type":"object"}

Two different schemas now compare equal. It needs a hand-written schema to trigger, since jsonschema.For never emits those keywords, so I am happy to see it as a follow-up.

Worth knowing if you take it: tracking an "am I in a schema position" flag is not enough on its own. A property can be named default, and suppressing normalization under it reintroduces the original bug:

{"properties":{"default":{"required":["b","a"]}}}
{"properties":{"default":{"required":["a","b"]}}}

You also need to treat properties, patternProperties, $defs, definitions and dependentSchemas as name-to-subschema maps. I tried this locally and it works out at roughly 45 lines.

Approving — the fix is correct for what it claims and the blast radius is one function with one caller.

@jjsasha63
jjsasha63 merged commit 11b0873 into google:main Sep 1, 2026
14 checks passed
kdroste-google pushed a commit to kdroste-google/adk-go that referenced this pull request Sep 29, 2026
…oogle#1138)

Static schema validation compares an edge's upstream output schema against
the downstream input schema by canonicalizing both and testing byte equality.
CanonicalSchemaJSON sorted object keys but kept arrays in source order, so
`required` was compared order-sensitively. jsonschema.For builds `required`
in struct-field declaration order, so two otherwise identical schemas whose
fields are declared in a different order canonicalized differently and were
rejected with "schema mismatch on edge ...".

Sort the unordered set-valued keywords (`required` and a `type` union) when
canonicalizing so equivalent schemas compare equal. Arrays for other keywords
keep their order, since keywords such as prefixItems are order-significant.

Co-authored-by: Sasha <[email protected]>
li-xuankun312 added a commit to li-xuankun312/einsteinOS that referenced this pull request Sep 29, 2026
…oogle#1138)

Static schema validation compares an edge's upstream output schema against
the downstream input schema by canonicalizing both and testing byte equality.
CanonicalSchemaJSON sorted object keys but kept arrays in source order, so
`required` was compared order-sensitively. jsonschema.For builds `required`
in struct-field declaration order, so two otherwise identical schemas whose
fields are declared in a different order canonicalized differently and were
rejected with "schema mismatch on edge ...".

Sort the unordered set-valued keywords (`required` and a `type` union) when
canonicalizing so equivalent schemas compare equal. Arrays for other keywords
keep their order, since keywords such as prefixItems are order-significant.

Co-authored-by: Sasha <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants