colinhacks/zod · error · Error

Invalid discriminated union option at index

Error message

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

What it means

Thrown inside the cached disc() function of a discriminated union when, for a given option o, `o._zod.propValues?.[def.discriminator]` is absent or empty. Unlike error 42 (which fires when an option has no propValues at all), this fires specifically when the option does have propValues but none are published under the configured discriminator key — i.e. the option exists but does not declare a literal value at that exact key.

Solutions

  1. Make sure every option has a literal value (z.literal() or z.enum()) at the exact discriminator key.
  2. Double-check the discriminator key spelling matches across all options.
  3. Avoid marking the discriminator field .optional(); every option must commit to a concrete literal.
  4. Print def.discriminator and each option's propValues keys to locate the mismatched branch.

Example fix

// before
const U = z.discriminatedUnion("type", [
  z.object({ type: z.literal("circle"), r: z.number() }),
  z.object({ type: z.string(), w: z.number(), h: z.number() }), // not a literal
]);
// after
const U = z.discriminatedUnion("type", [
  z.object({ type: z.literal("circle"), r: z.number() }),
  z.object({ type: z.literal("rect"), w: z.number(), h: z.number() }),
]);
Defensive patterns

Strategy: validation

Validate before calling

function buildDiscUnion(disc: string, options: z.ZodObject[]) {
  for (const o of options) {
    const vals = (o as any)._zod.propValues?.[disc];
    if (!vals || vals.size === 0) {
      throw new Error(`Option is missing a literal at discriminator key "${disc}".`);
    }
  }
  return z.discriminatedUnion(disc, options);
}

Type guard

function optionHasDiscriminator(option: unknown, disc: string): boolean {
  const vals = (option as any)?._zod?.propValues?.[disc];
  return !!vals && vals.size > 0;
}

Prevention

When it happens

Trigger: Calling z.discriminatedUnion("kind", [...]) where at least one option's object shape lacks a literal at the "kind" field, or declares it as a non-literal type (z.string() instead of z.literal()). Also triggered by typos in the discriminator key name ("type" vs "kind"), or by an option whose discriminator field is optional (z.literal("a").optional()) so propValues may be treated as not contributing a value.

Common situations: Discriminator key renamed in one branch but not others; one option omits the discriminator field entirely; field declared as z.string() instead of z.literal()/z.enum(); copy-paste of option schemas where the literal was forgotten; optional discriminator fields.

Related errors


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

Appendix: source

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

        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) {
          if (map.has(v)) {
            throw new Error(`Duplicate discriminator value "${String(v)}"`);
          }
          map.set(v, o);
        }
      }
      return map;
    });

    inst._zod.parse = (payload, ctx) => {
      const input = payload.value;
      if (!util.isObject(input)) {
        payload.issues.push({
          code: "invalid_type",

          expected: "object",
          input,

View on GitHub (pinned to 2d90846af9)