Skip to content

fix(generator): leave union-declared required names to the variant - #103

Merged
lightsofapollo merged 2 commits into
gpu-cli:mainfrom
gilesw:fix/allof-union-owned-required
Oct 12, 2026
Merged

lightsofapollo merged 2 commits into
gpu-cli:mainfrom
gilesw:fix/allof-union-owned-required

Conversation

@gilesw

@gilesw gilesw commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Summary

When an object pairs shared fields with a union, the generator emits a base struct plus a flattened variant. A required name that the shared fields don't declare got a synthesized required serde_json::Value field on the base struct, even when the union's branches declare that name with a real type.

Cloudflare's dns-records_dns-record-response hits this. Its outer required list is name, type, content, data, proxied, ttl, settings, comment, tags. All of those are declared only inside the per-record-type branches, and data exists only on CAA, SRV, HTTPS and similar records. The result:

  • every A, AAAA, CNAME, MX, NS, PTR, TXT and OPENPGPKEY record fails to deserialize with missing field `data`
  • name, content, ttl, type and the other fields are typed serde_json::Value on the base struct, even though the branch structs already carry them typed (for example content: Option<std::net::Ipv4Addr> on ARecord)

The bug occurs in both forms the schema takes: the current spec uses allOf: [anyOf: [...], {properties}], and the corpus-pinned spec puts properties and anyOf side by side.

This PR leaves a required name to the variant when any branch reachable through the union declares it. The branch types the field and keeps its own optionality. A required name that no shared member or branch declares still gets the existing placeholder.

Generated compatibility

  • Generated model or method signatures: for affected schemas, the base struct loses the untyped fields that the variant already carries. In the corpus this is only Cloudflare: DnsRecordsDnsRecordResponse and four DNS operation Response4xxResult types that embed it. types.rs shrinks by 210 lines. No other spec changes.
  • Query/path/header/body wire behavior: inputs that decoded before still decode, and serialization emits the same keys. Inputs that omit a branch-only field, such as data on an A record, now decode.
  • Generated runtime dependencies or features: none.
  • Configuration defaults or migrations: none.
  • Remaining unsupported OpenAPI shapes: none introduced.

Validation

  • Added a reduced Cloudflare-shaped fixture to tests/allof_object_union_preservation_test.rs in both forms: RecordResponse (allOf) and SiblingRecordResponse (sibling properties + anyOf). It checks that the base struct no longer owns the branch-declared names, and round-trips A, MX and CAA records through a compiled scratch crate. Both tests fail without the change with missing field `data` .
  • cargo fmt --check
  • cargo clippy --all-features -- -D warnings
  • cargo nextest run --all-features --no-fail-fast: 726 run, all pass. In an earlier run, required_undeclared_members_round_trip_untyped_and_typed_values failed while other cargo builds were running on the machine. It passed when rerun alone and in the full rerun.
  • cargo test --doc --all-features and cargo doc --no-deps --all-features 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

  • Smallest review path: union_declared_property_names and its two call sites in src/analysis.rs: the reconciliation at the end of analyze_allof_composition, and the end of analyze_object_with_variants. Then the new fixture.
  • Lazy analysis: the helper analyzes a union's $ref branch targets when they aren't cached yet, the same way merge_analyzed_object_properties resolves references. Without that, $ref-only unions report no declared names.
  • Base catch-all map: in the sibling form, the base had switched to an untyped additional_properties map only to hold the placeholders. Once those placeholders go, it returns to closed, unless the schema declares additionalProperties or its examples or minProperties/maxProperties need the map.
  • Found while evaluating the generator for a Rust Cloudflare CLI, the same work that led to fix(generator): accept wire-equivalent encodings when selecting a union branch #82. With this change, a zones and DNS subset of the current Cloudflare spec decodes list responses into DnsRecordsARecord, DnsRecordsMXRecord and so on, without patching the spec.

An object that pairs shared fields with a union, through allOf or as
sibling properties and anyOf/oneOf, synthesized a required
serde_json::Value field for every required name absent from the shared
properties, even when the union's branches declare it. Cloudflare's
dns-records_dns-record-response lists `data` as required though only
CAA, SRV and similar record types carry it, so A and MX records failed
to deserialize with "missing field `data`", and name, content and ttl
were untyped.

Required names declared by any branch reachable through the union are
now left to the variant, which types them and keeps each branch's own
optionality. Names declared nowhere keep the existing placeholder.

The corpus change is confined to Cloudflare's DNS record response types.
@vercel

vercel Bot commented Oct 12, 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
lightsofapollo merged commit 0b6658c into gpu-cli:main Oct 12, 2026
12 of 13 checks passed
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