Skip to content

fix: an unknown discriminator value no longer fails the whole decode - #41

Merged
giraffesyo merged 1 commit into
canaryfrom
fix/unknown-discriminator-preserves-payload
Aug 4, 2026
Merged

fix: an unknown discriminator value no longer fails the whole decode#41
giraffesyo merged 1 commit into
canaryfrom
fix/unknown-discriminator-preserves-payload

Conversation

@giraffesyo

Copy link
Copy Markdown
Member

Fixes #39

The bug

A generated discriminated union's UnmarshalJSON ended in:

default:
	return fmt.Errorf("unknown kind value: %q", disc.Kind)

Adding a member to a oneOf is normally an additive, backward-compatible change — under an untyped decode a new variant is simply inert until the client is rebuilt. Here it broke every deployed client at decode time, with no server-side signal. And since unions are usually list elements, one unrecognized entry in a hundred failed the entire response, so the client couldn't process the variants it did understand.

The fix

An unrecognized discriminator value is preserved rather than rejected. The wrapper gains two unexported fields and three accessors:

type Shape struct {
	Value any

	unknownDiscriminator string
	raw                  json.RawMessage
}

func (u Shape) IsUnknownVariant() bool         // Value == nil && raw present
func (u Shape) UnknownDiscriminator() string   // the unrecognized value, else ""
func (u Shape) Raw() json.RawMessage           // original JSON, else nil
  • UnmarshalJSON's default branch stores the discriminator value and a copy of the payload, and returns nil.
  • MarshalJSON returns those bytes verbatim for an unknown variant, so a decode/encode round trip is lossless.
  • Known branches now assign *u = Shape{Value: v}, so a reused variable can't keep stale raw from a prior decode.

Unions without a discriminator are unchanged — they still error when no variant matches, since there's nothing to identify the payload by. Output for those types is byte-identical to before.

Caller-side shape

for _, shape := range shapes {
    if shape.IsUnknownVariant() {
        log.Printf("skipping unsupported shape %q", shape.UnknownDiscriminator())
        continue // shape.Raw() still holds the original JSON
    }
    switch v := shape.Value.(type) { ... }
}

A caller that wants the old strictness can check IsUnknownVariant() and fail; a caller that doesn't gets the additive behavior spec authors assume.

Trade-off worth naming

A payload missing the discriminator property entirely now decodes as an unknown variant (empty discriminator) instead of erroring. That's the same code path as an unrecognized value, and IsUnknownVariant() lets a strict caller reject it.

Tests

TestE2E_InlineUnionRuntimeDispatch — which compiles and runs the generated client — is extended: an unmapped shapeType now decodes, reports IsUnknownVariant() / UnknownDiscriminator() == "hexagon", and re-marshals with its "sides":6 payload intact. Its previous assertion that this errored is replaced.

New TestUnknownVariantDoesNotFailSiblings covers the issue's central complaint directly: a collection holding one circle and one unknown triangle decodes successfully, with the circle typed and the triangle flagged.

README documents the behavior. Full suite and go vet pass.

A generated discriminated union returned an error for any variant it did not
know, so adding a member to a oneOf broke every client build that predated it.
Because unions are commonly list elements, a single unrecognized entry failed
the entire response and the recognized entries went with it.

The unknown variant is now preserved instead: Value stays nil, the original
JSON is kept on the wrapper, and MarshalJSON returns those bytes unchanged so a
re-marshal is lossless. IsUnknownVariant, UnknownDiscriminator, and Raw let
callers handle the case explicitly.

Unions without a discriminator still error when no variant matches.

Fixes #39
@giraffesyo
giraffesyo merged commit ff8808c into canary Aug 4, 2026
@giraffesyo
giraffesyo deleted the fix/unknown-discriminator-preserves-payload branch August 4, 2026 23:24
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.

An unknown discriminator value fails the whole decode, so adding a oneOf variant breaks existing clients

1 participant