Skip to content

Fix discriminator type on unions#2453

Merged
mromaszewicz merged 3 commits into
mainfrom
fix/issue-2297
Jul 12, 2026
Merged

Fix discriminator type on unions#2453
mromaszewicz merged 3 commits into
mainfrom
fix/issue-2297

Conversation

@mromaszewicz

@mromaszewicz mromaszewicz commented Jul 12, 2026

Copy link
Copy Markdown
Member

Closes: #2297

The From*/Merge* union helpers assigned the discriminator value as a raw string literal to the variant's field (v.Code = "resource_exists"), which does not compile when the discriminator property is optional and renders as a pointer to a named enum type. The assignment also assumed the Go field name matches the JSON property name, and that the variant declares the field at all.

When the union struct does not itself declare the discriminator property, the value is now stamped into the marshaled JSON instead, via runtime.JSONMerge with the JSON property name. This is type-agnostic: it works for pointer fields, named enum types, x-go-name renames, absent fields, and import-mapped external variants. When the union struct does declare the property, the field assignment remains (it feeds the union's MarshalJSON), now taking an address through a typed local variable when the field is pointer-typed (new Property.IsPointer helper).

Output is unchanged for unions without discriminators and for the existing non-pointer discriminator cases, except that From* on a discriminated union now emits JSON with the same alphabetical key order Merge* always produced.

…fields

Closes: #2297

The From*/Merge* union helpers assigned the discriminator value as a raw
string literal to the variant's field (v.Code = "resource_exists"), which
does not compile when the discriminator property is optional and renders
as a pointer to a named enum type. The assignment also assumed the Go
field name matches the JSON property name, and that the variant declares
the field at all.

When the union struct does not itself declare the discriminator property,
the value is now stamped into the marshaled JSON instead, via
runtime.JSONMerge with the JSON property name. This is type-agnostic: it
works for pointer fields, named enum types, x-go-name renames, absent
fields, and import-mapped external variants. When the union struct does
declare the property, the field assignment remains (it feeds the union's
MarshalJSON), now taking an address through a typed local variable when
the field is pointer-typed (new Property.IsPointer helper).

Output is unchanged for unions without discriminators and for the
existing non-pointer discriminator cases, except that From* on a
discriminated union now emits JSON with the same alphabetical key order
Merge* always produced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mromaszewicz
mromaszewicz requested a review from a team as a code owner July 12, 2026 17:19
@mromaszewicz mromaszewicz added the bug Something isn't working label Jul 12, 2026
@mromaszewicz mromaszewicz changed the title Fix discriminator stamping for pointer-typed and named discriminator … Fix discriminator type on unions Jul 12, 2026
Move as much logic as possible into Go code, instead of complicating
templates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes discriminator stamping in generated union helpers (From*/Merge*) to work correctly when the discriminator property is optional and rendered as a pointer to a named enum type; the old code mutated the variant struct by direct field assignment which doesn't compile for pointer-typed fields. The fix moves all discriminator injection to runtime.JSONMerge at the JSON level (type-agnostic), while additionally assigning the struct field when the union itself declares the discriminator property — using a new Property.IsPointer() helper and an addressable local variable for pointer fields.

  • pkg/codegen/schema.go adds DiscriminatorStampFor (pre-computes the value, JSON patch literal, and optional struct-field reference) and IsPointer() on Property.
  • union.tmpl replaces the old nested range/compare logic with a single $stamp value per element, emitting the pointer-assignment branch when needed.
  • Three new test scenarios in internal/test/aggregates/oneof/ cover pointer-on-variant (ConflictError), pointer-on-union (PetByKind), and x-go-name-renamed field (RenamedPetByKind), each exercising Discriminator() and ValueByDiscriminator() after From*.

Confidence Score: 5/5

Safe to merge; the change is a targeted bug fix with well-scoped regenerated output and full test coverage for the three new code paths.

The template refactor is clean, the new DiscriminatorStampFor logic is straightforward, and both the JSON-stamp-only path and the struct-field+stamp path are exercised by new tests that verify Discriminator() and ValueByDiscriminator() immediately after From*. The only noteworthy asymmetry is that From* does not guard against a failed JSONMerge before assigning t.union, while Merge* does — but since the second argument to JSONMerge is always a hardcoded literal, this path cannot fail in practice.

No files require special attention.

Important Files Changed

Filename Overview
pkg/codegen/templates/union.tmpl Core template change: switches From*/Merge* discriminator stamping from struct-field mutation to JSONMerge; adds pointer-typed field handling via IsPointer branch; minor inconsistency in From* where JSONMerge error is not checked before assigning t.union
pkg/codegen/schema.go Adds IsPointer() on Property (strings.HasPrefix on GoTypeDef) and DiscriminatorStampFor() on Schema; logic correctly gates on mapping 1:1 coverage, matches discriminator by JSON property name (not Go name), and produces a pre-baked JSONPatch literal
internal/test/aggregates/oneof/discriminator.gen.go New types and From*/Merge* helpers for ConflictError (JSON-stamp-only path) and PetByKind/RenamedPetByKind (struct-field + JSON-stamp path); Discriminator() reads from t.union which now includes the discriminator key after From*, fixing the issue
internal/test/aggregates/oneof/components.gen.go Regenerated: From*/Merge* for existing discriminated unions now add error check on json.Marshal and stamp discriminator via JSONMerge instead of field assignment; changes are scoped to discriminated unions and consistent across all affected types
internal/test/aggregates/oneof/discriminator_test.go Three new test cases cover the pointer-discriminator-on-variant path (ConflictError), pointer-discriminator-on-union path (PetByKind), and renamed-field path (RenamedPetByKind); all exercise Discriminator() and ValueByDiscriminator() after From*, fully covering the fix
internal/test/aggregates/oneof/spec_discriminator.yaml Adds three new schema scenarios (ConflictError, PetByKind, RenamedPetByKind) with appropriate discriminator mappings; anchored via path operations to prevent pruning; correctly placed in the existing aggregates/oneof fixture
internal/test/schemas/nullable/spec31/types.gen.go Regenerated DiscriminatedPet From*/Merge* methods: removes variant field mutation, adds error check on json.Marshal, stamps discriminator via JSONMerge; change is consistent with the template fix

Reviews (2): Last reviewed commit: "Address code review comments" | Re-trigger Greptile

Comment thread pkg/codegen/templates/union.tmpl
@mromaszewicz
mromaszewicz merged commit e66004b into main Jul 12, 2026
28 checks passed
@mromaszewicz
mromaszewicz deleted the fix/issue-2297 branch July 12, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

discriminator helpers generate invalid assignments for pointer-typed Code fields

1 participant