colinhacks/zod · error · Error

A discriminator value for key

Error message

A discriminator value for key `${discriminator}` could not be extracted from all schema options

What it means

Thrown at schema-construction time by z.discriminatedUnion (packages/zod/src/v3/types.ts:3200) when getDiscriminator returns no usable values for one of the option schemas. For a discriminated union, every option MUST expose the discriminator key as a literal (or enum/nativeEnum/const) value so Zod can build its lookup map; an option where the discriminator is optional, missing, or a dynamic type yields an empty value list and trips this error.

Solutions

  1. Ensure every option defines the discriminator key as `z.literal('value')` (or z.enum / z.nativeEnum) — not z.string(), not optional.
  2. Find the offending option by logging each option's shape[discriminator] before constructing the union.
  3. If a branch genuinely has no fixed discriminator value, restructure the schema as a plain z.union instead of z.discriminatedUnion.
  4. If the discriminator lives behind a transform or preprocess, hoist it out so it is a direct literal field.

Example fix

// before
const A = z.object({ type: z.literal('a'), x: z.number() });
const B = z.object({ y: z.string() }); // missing discriminator
const U = z.discriminatedUnion('type', [A, B]);

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

Strategy: validation

Validate before calling

import { z, ZodType, ZodFirstPartyTypeKind } from 'zod';

function getLiteralValues(s: ZodType): unknown[] {
  const def = (s as any)._def;
  if (!def) return [];
  switch (def.typeName) {
    case ZodFirstPartyTypeKind.ZodLiteral:
      return [def.value];
    case ZodFirstPartyTypeKind.ZodEnum:
      return [...def.values];
    case ZodFirstPartyTypeKind.ZodNativeEnum:
      return Object.values(def.values).filter((v: unknown) => typeof v === 'string' || typeof v === 'number');
    case ZodFirstPartyTypeKind.ZodUndefined:
      return [undefined];
    case ZodFirstPartyTypeKind.ZodNull:
      return [null];
    default:
      return [];
  }
}

function assertAllOptionsDiscriminate(discriminator: string, options: z.ZodType[]) {
  const missing = options.filter((o) => getLiteralValues((o as any).shape?.[discriminator]).length === 0);
  if (missing.length) {
    throw new Error(`Option(s) at indices ${missing.map((_, i) => i)} lack a literal discriminator '${discriminator}'`);
  }
}

Type guard

function isLiteralField(field: unknown): boolean {
  if (!field || typeof field !== 'object') return false;
  const tn = (field as any)._def?.typeName;
  return (
    tn === 'ZodLiteral' ||
    tn === 'ZodEnum' ||
    tn === 'ZodNativeEnum' ||
    tn === 'ZodUndefined' ||
    tn === 'ZodNull' ||
    tn === 'ZodBoolean' // boolean yields true/false literals
  );
}

Try / catch

null

Prevention

When it happens

Trigger: Calling `z.discriminatedUnion('type', [A, B, C])` where at least one option lacks the `type` field, has it as `z.string()` instead of a literal, marks it `.optional()`, or nests the discriminator inside a transform/lazy. Any option whose discriminator shape is not a plain literal/enum fails extraction.

Common situations: Forgetting to add the discriminator field to one branch of a tagged union; typing the discriminator as z.string() instead of z.literal('x'); making the discriminator optional in one variant; refactoring shared base objects where the discriminator key was dropped; mixing in a ZodAny or ZodUnknown as the discriminator.

Related errors


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

Appendix: source

Thrown at packages/zod/src/v3/types.ts:3200

  static create<
    Discriminator extends string,
    Types extends readonly [
      ZodDiscriminatedUnionOption<Discriminator>,
      ...ZodDiscriminatedUnionOption<Discriminator>[],
    ],
  >(
    discriminator: Discriminator,
    options: Types,
    params?: RawCreateParams
  ): ZodDiscriminatedUnion<Discriminator, Types> {
    // Get all the valid discriminator values
    const optionsMap: Map<Primitive, Types[number]> = new Map();

    // try {
    for (const type of options) {
      const discriminatorValues = getDiscriminator(type.shape[discriminator]);
      if (!discriminatorValues.length) {
        throw new Error(
          `A discriminator value for key \`${discriminator}\` could not be extracted from all schema options`
        );
      }
      for (const value of discriminatorValues) {
        if (optionsMap.has(value)) {
          throw new Error(`Discriminator property ${String(discriminator)} has duplicate value ${String(value)}`);
        }

        optionsMap.set(value, type);
      }
    }

    return new ZodDiscriminatedUnion<
      Discriminator,
      // DiscriminatorValue,
      Types
    >({
      typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion,

View on GitHub (pinned to 2d90846af9)