Skip to content

fix(go): disambiguate union discriminant field when it collides with a member name#16320

Merged
iamnamananand996 merged 6 commits into
mainfrom
devin/1780931156-go-sse-wire-tests
Jun 11, 2026
Merged

fix(go): disambiguate union discriminant field when it collides with a member name#16320
iamnamananand996 merged 6 commits into
mainfrom
devin/1780931156-go-sse-wire-tests

Conversation

@iamnamananand996

Copy link
Copy Markdown
Member

Description

Fixes the server-sent-event-examples:with-wire-tests go-sdk seed fixture by resolving a duplicate identifier bug in v1's discriminated union generation.

Changes Made

  • Added unionDiscriminantFieldName() helper in generators/go/internal/generator/model.go that detects when the discriminant's exported field name collides with a union member field name and returns a collision-free name (e.g. EventType instead of Event)
  • Applied the helper at both sites that compute the discriminant field name: the VisitUnion struct emission and the getTypeFieldsForUnion getter generation
  • Removed server-sent-event-examples:with-wire-tests from allowedFailures in seed/go-sdk/seed.yml
  • Added changelog entry

Root cause: StreamEventContextProtocol has discriminant value: event and a union member also keyed event (→ EventEvent). Both map to Go field name Event, producing duplicate struct fields and duplicate getter methods with conflicting return types.

Fix: The discriminant field is renamed to EventType (with a loop for further disambiguation if needed). Member fields and their public accessors (GetEvent(), NewStreamEventContextProtocolFromEvent()) are preserved. The JSON wire tag still uses the original wire value "event".

Testing

  • pnpm seed test --generator go-sdk --fixture server-sent-event-examples --local --skip-scripts passes 1/1
  • go build ./... passes on v1
  • Generated completions.go compiles with no duplicate identifiers
  • Wire tests pass (struct field EventType string, getter GetEventType() string for discriminant; Event *EventEvent, getter GetEvent() *EventEvent for member)

Link to Devin session: https://app.devin.ai/sessions/6ba63e4405694aa8ab8875f0dbfd9afc
Requested by: @iamnamananand996

…a member name

When a discriminated union's discriminant property name matches one of its
member discriminant values (e.g. discriminant 'event' with a member also
keyed 'event'), v1 emitted duplicate struct fields and getters that would
not compile. The discriminant field is now given a unique collision-free
name (e.g. EventType) while member fields and their public accessors are
preserved unchanged.
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot marked this pull request as ready for review June 8, 2026 15:20
@devin-ai-integration devin-ai-integration Bot requested a review from amckinney as a code owner June 8, 2026 15:20

@claude claude 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@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 found 1 potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +394 to +400
taken := make(map[string]struct{}, len(union.Types)+len(union.BaseProperties))
for _, unionType := range union.Types {
taken[goExportedFieldName(unionType.DiscriminantValue.Name.PascalCase.UnsafeName)] = struct{}{}
}
for _, property := range union.BaseProperties {
taken[goExportedFieldName(property.Name.Name.PascalCase.UnsafeName)] = struct{}{}
}

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.

🟡 unionDiscriminantFieldName omits union.Extends properties from collision check

The unionDiscriminantFieldName function builds its taken set from union.Types and union.BaseProperties (lines 395-400), but it does not include properties inherited via union.Extends. In VisitUnion, visitObjectProperties is called for each extended type (model.go:423-431), and it writes fields directly to the struct via t.writer.P(fieldName, " ", goType) at model.go:1353. If the discriminant's Go exported name matches an extended property's exported name, the collision goes undetected, and duplicate struct fields are emitted — the exact same compilation error this PR aims to fix.

Example scenario and relevant code paths

If a union extends an object that has a property named e.g. "event", and the discriminant is also "event", the taken map won't contain the extended property's name, so collides is false and the base name is returned unchanged. The struct then gets two Event fields. The fix should collect extended property names into the taken map, recursively resolving nested extends via t.writer.types[extend.TypeId].Shape.Object.

The getTypeFieldsForUnion function correctly processes extends at model.go:1497-1499, confirming these fields are part of the union's struct.

Prompt for agents
The unionDiscriminantFieldName function at generators/go/internal/generator/model.go:392-415 needs to also collect field names from union.Extends into the taken map. The union.Extends field contains DeclaredTypeName references whose TypeId can be used to look up the object type declaration. However, this function is a standalone function (not a method on typeVisitor), so it doesn't have access to the types map. You need to either:

1. Add a types parameter (map[common.TypeId]*ir.TypeDeclaration) to unionDiscriminantFieldName, then recursively walk each extended object's properties (and their extends) collecting goExportedFieldName values into taken. This mirrors the recursive approach used in visitObjectProperties at model.go:1321-1326.

2. Or convert unionDiscriminantFieldName to a method on typeVisitor so it can access t.writer.types directly.

The recursive collection should mirror what visitObjectProperties does: for each extend, resolve the object via types[extend.TypeId].Shape.Object, iterate its Properties collecting goExportedFieldName(property.Name.Name.PascalCase.UnsafeName), and recurse into that object's own Extends.

All call sites (model.go:419 and model.go:1490) would need to be updated to pass the types map if using approach 1.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

Good catch — fixed in f857d6e. unionDiscriminantFieldName is now a typeVisitor method and recursively collects exported field names from union.Extends (and nested extends) into the taken set, with nil/cycle guards, so a discriminant colliding with an inherited property is now disambiguated too. The server-sent-event-examples fixture output is unchanged (no extends collision there), confirming this is purely additive coverage.

…heck

Make unionDiscriminantFieldName a typeVisitor method so it can also account
for properties inherited via union.Extends (recursively, with nil/cycle
guards) when disambiguating the discriminant field name. Previously only
member and base-property names were checked, so a discriminant colliding
with an extended property could still emit duplicate struct fields.
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-06-11T05:33:09Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
go-sdk square 137s (n=5) 287s (n=5) 131s -6s (-4.4%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-06-11T05:33:09Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-06-11 14:20 UTC

@iamnamananand996 iamnamananand996 merged commit 2f84ece into main Jun 11, 2026
79 checks passed
@iamnamananand996 iamnamananand996 deleted the devin/1780931156-go-sse-wire-tests branch June 11, 2026 15:34
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