colinhacks/zod · error · Error

Invalid discriminated union option at index

Error message

Invalid discriminated union option at index "${def.options.indexOf(option)}"

What it means

Thrown inside the lazy propValues initializer of a discriminated union when iterating def.options and an option's _zod.propValues is empty or undefined. propValues are the literal/enum values a schema can produce (used for discriminator detection); an option that contributes none — e.g. a plain z.any(), z.unknown(), z.object({}) with no literal fields, or a schema type that never publishes propValues — cannot participate in discrimination and is rejected when propValues is first accessed.

Solutions

  1. Ensure every option is an object schema containing a literal (or enum/literals) at the discriminator key.
  2. Replace any z.any()/z.unknown()/empty-object option with a concrete schema that has the discriminator field.
  3. If you genuinely need a non-discriminable option, switch from z.discriminatedUnion() to z.union().
  4. Inspect each option's _zod.propValues in a scratch script to find which one is empty.

Example fix

// before
const U = z.discriminatedUnion("type", [
  z.object({ type: z.literal("a"), x: z.number() }),
  z.object({ y: z.string() }), // missing discriminator
]);
// after
const U = z.discriminatedUnion("type", [
  z.object({ type: z.literal("a"), x: z.number() }),
  z.object({ type: z.literal("b"), y: z.string() }),
]);
Defensive patterns

Strategy: validation

Validate before calling

function buildDiscriminatedUnion(disc: string, options: z.ZodObject[]) {
  for (const o of options) {
    const pv = (o as any)._zod.propValues;
    if (!pv || Object.keys(pv).length === 0) {
      throw new Error(`Option is not discriminable (no propValues); use a concrete object schema.`);
    }
  }
  return z.discriminatedUnion(disc, options);
}

Type guard

function hasPropValues(schema: unknown): boolean {
  const pv = (schema as any)?._zod?.propValues;
  return !!pv && Object.keys(pv).length > 0;
}

Prevention

When it happens

Trigger: Calling z.discriminatedUnion("type", [SchemaA, z.object({})]) where one option has no discriminable literal fields; including z.any(), z.unknown(), z.never(), a non-literal z.string(), or a passthrough object as an option; wrapping the discriminator field in a transform that strips propValues.

Common situations: Mistakenly mixing a generic catch-all option into a discriminated union; refactoring an option to remove the literal discriminator field; using z.literal-less enums where the discriminator key was renamed in one branch; upgrading from z.union() (which has no discriminator requirement) to z.discriminatedUnion() without ensuring every option has a literal at the discriminator key.

Related errors


AI-assisted analysis of colinhacks/zod@2d90846af9 (2026-08-11). Data as JSON: /api/errors/b7218630a8958ab0. Report an issue: GitHub.

Appendix: source

Thrown at packages/zod/src/v4/core/schemas.ts:2390

  Disc extends string = string,
> extends $ZodType {
  _zod: $ZodDiscriminatedUnionInternals<Options, Disc>;
}

export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
  /*@__PURE__*/
  core.$constructor("$ZodDiscriminatedUnion", (inst, def) => {
    def.inclusive = false;

    $ZodUnion.init(inst, def);

    const _super = inst._zod.parse;
    util.defineLazy(inst._zod, "propValues", () => {
      const propValues: util.PropValues = {};
      for (const option of def.options) {
        const pv = option._zod.propValues;
        if (!pv || Object.keys(pv).length === 0)
          throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(option)}"`);
        for (const [k, v] of Object.entries(pv!)) {
          if (!propValues[k]) propValues[k] = new Set();
          for (const val of v) {
            propValues[k].add(val);
          }
        }
      }
      return propValues;
    });

    const disc = util.cached(() => {
      const opts = def.options as $ZodTypeDiscriminable[];
      const map: Map<util.Primitive, $ZodType> = new Map();
      for (const o of opts) {
        const values = o._zod.propValues?.[def.discriminator];
        if (!values || values.size === 0)
          throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(o)}"`);
        for (const v of values) {

View on GitHub (pinned to 2d90846af9)