colinhacks/zod · error · Error

Discriminator property

Error message

Discriminator property ${String(discriminator)} has duplicate value ${String(value)}

What it means

Thrown at schema-construction time by z.discriminatedUnion (packages/zod/src/v3/types.ts:3206) when two option schemas produce the same discriminator value. The discriminated union dispatches on the discriminator, so each literal value must map to exactly one option; a collision makes routing ambiguous and Zod refuses to build the optionsMap.

Solutions

  1. Give each option a unique discriminator literal value — the duplicate value is named verbatim in the error message.
  2. If two branches legitimately share a tag, merge them into a single option using z.union on the differing fields, or switch to z.union and disambiguate via refinement.
  3. Audit z.enum and multi-literal discriminators: each literal across all options must appear in exactly one option.
  4. Rebuild the union incrementally (add one option at a time) so the colliding pair is obvious.

Example fix

// before
const A = z.object({ type: z.literal('circle'), r: z.number() });
const B = z.object({ type: z.literal('circle'), side: z.number() }); // duplicate 'circle'
const Shape = z.discriminatedUnion('type', [A, B]);

// after
const B = z.object({ type: z.literal('square'), side: z.number() });
const Shape = z.discriminatedUnion('type', [A, B]);
Defensive patterns

Strategy: validation

Validate before calling

import { z, ZodType } from 'zod';

function assertUniqueDiscriminatorValues(discriminator: string, options: ZodType[]) {
  const seen = new Map<unknown, number>();
  for (const o of options) {
    const def = (o as any).shape?.[discriminator]?._def;
    const values = def?.values ?? (def?.value !== undefined ? [def.value] : []);
    for (const v of values) seen.set(v, (seen.get(v) ?? 0) + 1);
  }
  const dupes = [...seen.entries()].filter(([, n]) => n > 1);
  if (dupes.length) throw new Error(`Duplicate discriminator values: ${dupes.map(([k]) => JSON.stringify(k)).join(', ')}`);
}

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Calling `z.discriminatedUnion('kind', [A, B])` where both A and B expose `kind: z.literal('same')`, or where an option uses z.enum(['x','y']) and another uses z.literal('x'), producing overlapping values that the getDiscriminator walk collects into the same map key.

Common situations: Copy-pasting a branch and forgetting to change its discriminator literal; merging two schemas that share a tag value; refactoring an enum-valued discriminator where one literal now overlaps another branch; using z.union of literals that expands to values already used elsewhere.

Related errors


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

Appendix: source

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

  >(
    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,
      discriminator,
      options,
      optionsMap,
      ...processCreateParams(params),
    });
  }

View on GitHub (pinned to 2d90846af9)