colinhacks/zod · error · Error

A discriminator value for key `${discriminator}` could not b

Error message

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

What it means

Thrown by z.discriminatedUnion when getDiscriminator returns no extractable literal value for one of the options. The discriminator field must resolve to a concrete primitive (literal string/number/boolean, a single-value enum, etc.) on every option so the union can dispatch at runtime.

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

Solutions

  1. On every option, set the discriminator field to a concrete literal: `type: z.literal('a')`.
  2. If the tag is optional in some branch, decide on a default via z.literal and remove optionality at the discriminator key.
  3. Verify the discriminator key string actually exists on each option's `.shape`.

Example fix

// before
z.discriminatedUnion('type', [
  z.object({ type: z.string(), a: z.string() }), // no literal -> error
  z.object({ type: z.literal('b'), b: z.number() }),
]);

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

Strategy: validation

Validate before calling

import { getDiscriminator } from "zod"; // or replicate
function verifyDiscriminator(disc: string, options: z.ZodObject<any>[]) {
  for (const o of options) {
    const vals = o.shape[disc];
    if (!vals || !('value' in vals._def) && vals._def.values == null) {
      throw new Error(`Option missing literal discriminator '${disc}'`);
    }
  }
}

Type guard

function hasLiteralDiscriminator<K extends string>(
  o: z.ZodObject<any>,
  key: K
): boolean {
  const f = (o.shape as any)[key];
  return !!f && (f instanceof z.ZodLiteral || f instanceof z.ZodEnum || f instanceof z.ZodNativeEnum);
}

Prevention

When it happens

Trigger: z.discriminatedUnion('type', [A, B]) where one option's `type` field is a plain z.string() (no literal), optional and missing, an unsupported type (e.g. z.any(), z.array()), or nested behind an intermediary that getDiscriminator cannot unwrap.

Common situations: Building a tagged union where one branch forgot to constrain the tag (used `type: z.string()` instead of `type: z.literal('a')`); renaming a tag literal and forgetting a branch; discriminator hidden under z.optional or z.default.

Related errors


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