Skip to content

fix(generator): accept wire-equivalent encodings when selecting a union branch - #82

Merged
lightsofapollo merged 3 commits into
gpu-cli:mainfrom
gilesw:fix/union-branch-wire-equivalence
Sep 25, 2026
Merged

lightsofapollo merged 3 commits into
gpu-cli:mainfrom
gilesw:fix/union-branch-wire-equivalence

Conversation

@gilesw

@gilesw gilesw commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Union deserializers keep a branch only when re-serializing it reproduces the input. Two encodings that lose no information failed that check:

  • an integer in a number field: "ttl": 1 decodes into f64 and re-encodes as 1.0
  • an explicit null for an optional field: "comment": null decodes to None, which is skipped on output

oneOf compared with strict ==, so it failed on both; anyOf already tolerated the numeric case but treated an input null as lost. Together they reject every record of the Cloudflare DNS API, which types its integers as number and returns null for optional fields. For example, GET /zones/{zone_id}/dns_records/{dns_record_id} fails with:

complete shared-union input failed: no anyOf branch for DnsRecordsDnsRecordResponseAllOfVariant1 preserved the complete input; projected input failed: …

This PR keeps exact matching as the first pass. Only when no branch reproduces the input exactly does it accept a branch whose encoding differs solely by those two forms. Several equivalent oneOf branches are still reported as ambiguous.

Generated compatibility

  • Generated model or method signatures: unchanged. gen-diff.sh reports 0 public items added or removed across the corpus.
  • Query/path/header/body wire behavior: serialization is unchanged. Deserialization accepts strictly more inputs: any input that decoded before keeps the same branch, because the exact pass runs first and its result wins.
  • Generated runtime dependencies or features: none.
  • Configuration defaults or migrations: none.
  • Remaining unsupported OpenAPI shapes: none introduced.

Validation

  • Added a focused fixture and behavioral regression test: tests/union_branch_wire_equivalence_test.rs, a reduced Cloudflare-shaped allOf + anyOf + oneOf record. Three cases failed before the change. A guard case checks that a oneOf of integer vs number branches still selects integer for 1 and number for 1.5.
  • Reviewed every changed snapshot: the two tools_array union snapshots change only in the generated deserializer.
  • cargo fmt --check
  • cargo clippy --all-features -- -D warnings
  • cargo nextest run --all-features (681 run, all pass after the snapshot update), plus cargo test --doc --all-features and cargo doc with -Dwarnings
  • scripts/spec-compile.sh anthropic openai: both PASS + ROUNDTRIP
  • Refreshed tests/corpus-manifest.txt; --check is clean
  • Changelog entry under Unreleased

Notes for reviewers

  • Generated size. oneOf deserializers now carry the same inline comparison helpers that anyOf deserializers already emit. That is most of the gen-diff.sh churn (38 specs; for example vercel +83k/−27k lines, cloudflare +17k/−7k). If you would prefer it, I'm happy to follow up by emitting these helpers once per types.rs as a private module, in the same way as tri_state_serde. That would shrink output below today's size for both union kinds. I kept this PR to the behavior change.
  • Smallest review path: the two quote! attempt blocks, no_match, and preserves_complete_json_input in src/generator.rs; then the new test.
  • Found while evaluating the generator for a Rust Cloudflare CLI, using a 14-operation subset of the current Cloudflare spec. With this change, A, CAA and MX responses decode into their typed variants without patching the spec's number types.

…on branch

Union deserializers keep a branch only if re-serializing it reproduces the
input. An integer in a `number` field (re-encoded as `1.0`) and an
explicit `null` for an optional field (skipped on output) both failed
that check, so every Cloudflare DNS record was rejected.

oneOf compared with strict equality, and anyOf treated an input null as
lost. Both now fall back to a comparison that treats those encodings as
equal, but only when no branch reproduces the input exactly, so inputs
that decoded before keep their branch and ambiguity is still reported.
@vercel

vercel Bot commented Sep 25, 2026

Copy link
Copy Markdown

@gilesw is attempting to deploy a commit to the lbl-rd Team on Vercel.

A member of the Team first needs to authorize it.

@lightsofapollo

Copy link
Copy Markdown
Contributor

incorporating these changes into #83 - Thanks for the report + pr !

@lightsofapollo
lightsofapollo merged commit ec14c4d into gpu-cli:main Sep 25, 2026
1 check failed
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