Skip to content

fix(rust): type union base properties on variants that have no own properties - #17660

Open
wiebren wants to merge 1 commit into
fern-api:mainfrom
wiebren:fix/rust-union-inherited-message-variants
Open

fix(rust): type union base properties on variants that have no own properties#17660
wiebren wants to merge 1 commit into
fern-api:mainfrom
wiebren:fix/rust-union-inherited-message-variants

Conversation

@wiebren

@wiebren wiebren commented Sep 4, 2026

Copy link
Copy Markdown

Description

Linear ticket: n/a — found while running one OpenAPI document through fern's SDK generators
and comparing what each one produces.

A discriminated union with base properties generates constructors and discriminant-switching
getters that reference those properties on every variant — but the variant struct itself
is emitted as an empty braced variant whenever the variant has no properties of its own. The
enum then cannot compile:

error[E0559]: variant `SrsError::ProcessError` has no field named `message`
error[E0026]: variant `SrsError::ProcessError` does not have a field named `message`

An error taxonomy is the natural way to hit this: a union of error subtypes shares a base
message, and for many subtypes the type name is the information — they add no fields of
their own. One such union with nine message-only subtypes produced 18 compile errors in the
generated crate.

The repository's own seed output already exhibits the bug: the committed
seed/rust-sdk/property-access crate does not build —

Empty {},
...
pub fn get_normal(&self) -> &str {
    match self {
        ...
        Self::Empty { normal, .. } => normal,   // error[E0026]
$ cargo build   # on seed/rust-sdk/property-access as committed on main
error[E0026]: variant `UserOrAdminDiscriminated::Empty` does not have a field named `normal`
error[E0026]: variant `UserOrAdminDiscriminated::Empty` does not have a field named `foo`

Root cause

In generators/rust/model/src/union/UnionGenerator.ts two variant-emission paths skip
generateBaseProperties():

  • the noProperties variant shape, and
  • an inlined samePropertiesAsObject variant whose referenced object has an empty property
    list.

Both write Variant {},. Everything else already treats base properties as present on every
variant: generateImplementationBlock emits Self::Variant { field, .. } match arms for each
base-property getter across all variants, and the constructor for the empty inlined case
already assigns the base fields into the variant it just declared without them (that is the
E0559 above).

Changes Made

  • Emit the union's base properties on both empty-variant paths, keeping the plain Variant {}
    form when the union declares no base properties.
  • Let the noProperties constructor accept the base properties, mirroring what the empty
    inlined-object constructor already does.
  • Add the rust-union-base-properties test definition (language-prefixed, so only the rust
    generators run it): a union with a base message whose variants include an inlined object
    with an empty property list, an inlined object with its own property, and a noProperties
    variant.
  • Regenerate the two affected seed outputs (property-access, rust-union-base-properties).
    No other fixture output changes.

Testing

  • pnpm seed test --generator rust-sdk — 149/149 pass with the change.
  • cargo build + cargo test on both affected fixtures:
fixture before after
property-access error[E0026] ×2, does not build builds, 86 tests pass
rust-union-base-properties error[E0559], error[E0026] ×2, does not build builds, 86 tests pass
  • pnpm turbo run test --filter @fern-api/rust-model --filter @fern-api/rust-sdk — all unit
    tests pass.

Generated with Claude Code


Devin Review

…operties

A discriminated union's base properties are referenced on every variant by the
generated constructors and discriminant-switching getters, but the variant
struct itself was emitted as an empty braced variant when the variant had no
own properties (a noProperties variant, or an inlined referenced object with
an empty property list). The enum then fails to compile:

  error[E0559]: variant `...` has no field named `message`
  error[E0026]: variant `...` does not have a field named `message`

The committed property-access seed output already exhibits this: its `Empty`
variant is generated as `Empty {}` while `get_normal()`/`get_foo()` match
`Self::Empty { normal, .. }`, so the crate does not build.

Emit the base properties on those variants too, and let the noProperties
constructor accept them. Adds the rust-union-base-properties seed fixture
covering a union with base properties whose variants include an inlined
empty object and a noProperties variant; both affected fixtures now compile
and pass cargo test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CxDNCjqJycKTfzVWg2SeTJ

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Review Summary

Straightforward generator fix: empty union variants now carry the union's base properties, matching what the getters/constructors already assume. Seed fixtures regenerated and a new test definition added. One thing worth double-checking: the generated ValidationError variant in the new fixture emits #[serde(default)] field: String before message, which suggests base-property ordering/serde attributes may still be inconsistent, but that's pre-existing behavior outside these lines.

  • 🔵 1 suggestion(s)

To request another review, comment /ai-review on this pull request.

Comment on lines +372 to +382
if (this.unionTypeDeclaration.baseProperties.length > 0) {
// Base properties are typed on every variant (getters and
// constructors reference them), so they must be present even
// when the inlined type has no own properties.
writer.writeLine(` ${variantName} {`);
this.generateBaseProperties(writer);
writer.writeLine(` },`);
} else {
// Empty type: generate empty struct variant
writer.writeLine(` ${variantName} {},`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

This block is now identical to the noProperties branch above (lines 307-317). Worth extracting a small helper, e.g. writeEmptyVariant(writer, variantName), so the two paths can't drift.

@devin-ai-integration devin-ai-integration Bot 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.

🔍 Devin Review: 1 flag

Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)

Devin Review

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.

1 participant