close
Skip to content

Invalid discriminator errors in Zod 4 do not list possible options like Zod 3 #5670

Closed

Description

@patrickshipe

In Zod 3, if you received an error for an invalid discriminator, you received a list of possible options. In Zod 4, it just gives you a invalid_union code with no sub-errors or list of possible values. If the error map had access to the actual schema, it could theoretically use the discriminator to loop through the options, but this seems impossible at this point.

This is a pretty limiting regression in Zod 4, since without this information we can't tell users what they should be doing.

The script below logs the following:

Zod 4
[
  {
    code: 'invalid_union',
    errors: [],
    note: 'No matching discriminator',
    discriminator: 'type',
    path: [ 'type' ],
    message: 'Invalid input'
  }
]

Zod 3
[
  {
    code: 'invalid_union_discriminator',
    options: [ 'fruit', 'vegetable' ],
    path: [ 'type' ],
    message: "Invalid discriminator value. Expected 'fruit' | 'vegetable'"
  }
]
import { z } from 'zod';
import { z as z3 } from 'zod/v3';

console.log('Zod 4');
const schema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('fruit') }),
  z.object({ type: z.literal('vegetable') }),
]);

console.log(schema.safeParse({ type: 'hi' }).error?.issues);

console.log('Zod 3');

const schema2 = z3.discriminatedUnion('type', [
  z3.object({ type: z3.literal('fruit') }),
  z3.object({ type: z3.literal('vegetable') }),
]);

console.log(schema2.safeParse({ type: 'hi' }).error?.issues);

Activity

  1. pullfrog commented on Jan 28, 2026

    @pullfrog
    Contributor

    Issue Reproduction Confirmed ✅

    I reproduced the issue and verified the difference between Zod 3 and Zod 4:

    Zod 4 output:

    [
      {
        "code": "invalid_union",
        "errors": [],
        "note": "No matching discriminator",
        "discriminator": "type",
        "path": ["type"],
        "message": "Invalid input"
      }
    ]

    Zod 3 output:

    [
      {
        "code": "invalid_union_discriminator",
        "options": ["fruit", "vegetable"],
        "path": ["type"],
        "message": "Invalid discriminator value. Expected 'fruit' | 'vegetable'"
      }
    ]

    Analysis

    This is a valid regression. Zod 3 provided:

    1. A dedicated invalid_union_discriminator error code
    2. An options field listing valid discriminator values
    3. A descriptive error message

    Zod 4 consolidates this into the generic invalid_union code but loses the options information. The discriminator map (disc.value in packages/zod/src/v4/core/schemas.ts:2297-2312) already contains all valid discriminator values—they're just not being included in the error.

    Suggested Fix

    In packages/zod/src/v4/core/schemas.ts at line ~2337, add an options field:

    payload.issues.push({
      code: "invalid_union",
      errors: [],
      note: "No matching discriminator",
      discriminator: def.discriminator,
      options: Array.from(disc.value.keys()),  // ← Add this
      input,
      path: [def.discriminator],
      inst,
    });

    Then update packages/zod/src/v4/locales/en.ts to produce a better message when options is present on invalid_union issues.

    This would restore the Zod 3 behavior while keeping the unified invalid_union error code.

    Pullfrog  | Triggered by Pullfrog | Using Claude Code | View workflow run | pullfrog.com | 𝕏

  2. BrendanC23 commented on Jan 29, 2026

    @BrendanC23

    See #5086 for a related discussion.

  3. Danielchinasa commented on Feb 18, 2026

    @Danielchinasa
    Contributor

    I'd like to work on this issue. Can you assign it to me?

  4. added a commit that references this issue on Feb 18, 2026
    5e47a4d
  5. added a commit that references this issue on Apr 28, 2026
    2e5b23d
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions