Skip to content

fix: discriminatedUnion encode() with codec discriminator#5769

Merged
colinhacks merged 1 commit into
colinhacks:mainfrom
mahmoodhamdi:fix/discriminated-union-encode-codec
Apr 28, 2026
Merged

fix: discriminatedUnion encode() with codec discriminator#5769
colinhacks merged 1 commit into
colinhacks:mainfrom
mahmoodhamdi:fix/discriminated-union-encode-codec

Conversation

@mahmoodhamdi
Copy link
Copy Markdown
Contributor

Summary

Fixes #5593

When the discriminator field is a ZodCodec, encode() fails with "No matching discriminator" because the fast discriminator map only contains forward (decode) values while the encode input has backward (output) values.

Before

const codec1 = z.codec(z.literal(1), z.literal("one"), {
  decode: () => "one" as const,
  encode: () => 1 as const,
});
const schema = z.discriminatedUnion("type", [
  z.object({ type: codec1, value: z.string() }),
  // ...
]);
z.encode(schema, { type: "one", value: "hello" }); // ❌ "No matching discriminator"

After

z.encode(schema, { type: "one", value: "hello" }); // ✅ { type: 1, value: "hello" }

Approach

The discriminator map is precomputed from forward propValues — for codecs, these are the input schema values (1, 2). During encode(), the discriminator comes from the output schema ("one", "two"), so the fast-path lookup misses.

Rather than building a second backward discriminator map (which adds complexity and may not generalize to all codec patterns), this fix falls back to the union matching path when the fast discriminator lookup fails in backward direction. This is the same path that unionFallback: true already uses.

Files changed

  • packages/zod/src/v4/core/schemas.ts — 3-line change in $ZodDiscriminatedUnion.parse
  • packages/zod/src/v4/classic/tests/discriminated-unions.test.ts — new test case

Test plan

  • New test: encode with codec discriminator passes
  • All existing discriminated union tests pass (23/23)
  • Full test suite passes (3577 tests, 0 type errors)
  • Format and lint checks pass

When the discriminator field is a ZodCodec, the forward discriminator
values (used during decode) differ from the backward values (used during
encode). The fast discriminator map only contains forward values, so
encode() fails with "No matching discriminator".

Fall back to union matching when the fast-path fails in backward
direction, allowing codec-based discriminators to work correctly.

Fixes colinhacks#5593
@colinhacks
Copy link
Copy Markdown
Owner

@pullfrog review

Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

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

Reviewed — no issues found.

Clean, minimal fix. The fast discriminator path is still tried first, so non-codec discriminated unions pay no cost during encode(). The fallback to union matching only triggers when the fast lookup genuinely misses (the codec case). Test covers both directions.

Task list (6/6 completed)
  • Checkout PR and read the diff
  • Read and analyze the changed files in detail
  • Review linked issue #5593 for context
  • Verify correctness of the fix approach
  • Review tests for completeness
  • Submit review

Pullfrog  | View workflow run | Using Claude Opus𝕏

@colinhacks colinhacks merged commit c7a8ccc into colinhacks:main Apr 28, 2026
7 checks passed
@colinhacks
Copy link
Copy Markdown
Owner

Clean fix. The fast-path map is still tried first for encode(), so non-ZodCodec discriminatedUnions don't pay anything; falling back to the union matcher only when the lookup misses keeps the change small and avoids maintaining a parallel backward map. Thanks for the test coverage 👍

Note: this comment was produced by an AI coding assistant.

@colinhacks
Copy link
Copy Markdown
Owner

Landed in Zod 4.4

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.

discriminatedUnion fails on encode() when discriminator is a ZodCodec

2 participants