colinhacks/zod · error · Error

Invalid discriminated union option at index "${def.options.i

Error message

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

What it means

Thrown during construction of a discriminated union while lazily computing each option's `propValues` (the set of concrete values its literal/enum fields can take). A union option was supplied that has no `propValues` at all — meaning it is not an object schema with at least one discriminable (literal/enum) property. Every option in a discriminated union must be a discriminable object so the discriminator can be resolved. This fires at definition time (when `propValues` is first accessed, typically during first parse or toJSONSchema).

Source

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

  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 912f0f51b0)

Solutions

  1. Inspect the option at the reported index and ensure it is a `z.object({...})` with at least one property defined as `z.literal(...)`, `z.enum([...])`, or a set of `z.literal` unions.
  2. If a branch is genuinely a non-object/primitive, it cannot participate in a discriminated union — switch to `z.union([...])` instead, or restructure the data so every branch is an object carrying the discriminator.
  3. Add a literal discriminator field (e.g. `type: z.literal('foo')`) to the offending option so its propValues become non-empty.

Example fix

// before
const U = z.discriminatedUnion('type', [
  z.object({ type: z.literal('a'), value: z.string() }),
  z.string(), // not discriminable
]);

// after
const U = z.discriminatedUnion('type', [
  z.object({ type: z.literal('a'), value: z.string() }),
  z.object({ type: z.literal('b'), value: z.number() }),
]);
Defensive patterns

Strategy: validation

Validate before calling

import { z } from 'zod';
function assertDiscriminableOptions(discriminator, options) {
  options.forEach((opt, i) => {
    const pv = opt._zod?.propValues;
    if (!pv || Object.keys(pv).length === 0) {
      throw new Error(`Option ${i} is not discriminable (no literal/enum properties)`);
    }
    if (!(discriminator in pv)) {
      throw new Error(`Option ${i} is missing discriminator "${discriminator}"`);
    }
  });
}
// call before constructing the union:
assertDiscriminableOptions('type', [optA, optB]);

Type guard

import type { z } from 'zod';
function isDiscriminableObject(s: z.ZodType): boolean {
  const pv = (s as any)._zod?.propValues;
  return !!pv && Object.keys(pv).length > 0;
}

Try / catch

try {
  const U = z.discriminatedUnion('type', options);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Invalid discriminated union option')) {
    // log which option failed and fall back to z.union or fix the option
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a non-object schema (e.g. `z.string()`, `z.number()`, `z.any()`) as an option to `z.discriminatedUnion('type', [...])`, or an object schema whose properties are all non-literal/non-enum (so no propValues are collected). Also triggered by `z.never()` or empty `z.object({})` as an option.

Common situations: Migrating a plain `z.union([...])` to `z.discriminatedUnion(...)` where one branch was a primitive; refactoring shared options out and forgetting one branch still needs a literal discriminator field; tests that reuse a generic `z.object({})` placeholder.

Related errors


AI-assisted analysis of colinhacks/zod@912f0f51b0 (2026-08-03). Data as JSON: /data/errors/b7218630a8958ab0.json. Report an issue: GitHub.