colinhacks/zod · error · Error

Duplicate discriminator value

Error message

Duplicate discriminator value "${String(v)}"

What it means

Thrown while building the discriminator map of a discriminated union when map.has(v) is already true for a value v produced by the current option's propValues. Each discriminator value must uniquely route to a single option; two options claiming the same literal at the discriminator key would make dispatch ambiguous, so construction aborts with the offending value (coerced via String(v)).

Solutions

  1. Assign a unique literal value at the discriminator key for each option.
  2. Audit overlapping z.enum() members and remove duplicates across options.
  3. If two options legitimately share the same discriminator value, merge them into a single option or use z.union() instead.
  4. Add a unit test asserting the set of discriminator values has no duplicates.

Example fix

// before
const U = z.discriminatedUnion("type", [
  z.object({ type: z.literal("a"), x: z.number() }),
  z.object({ type: z.literal("a"), y: z.string() }), // duplicate "a"
]);
// after
const U = z.discriminatedUnion("type", [
  z.object({ type: z.literal("a"), x: z.number() }),
  z.object({ type: z.literal("b"), y: z.string() }),
]);
Defensive patterns

Strategy: validation

Validate before calling

function buildDiscUnion(disc: string, options: z.ZodObject[]) {
  const seen = new Set<unknown>();
  for (const o of options) {
    for (const v of ((o as any)._zod.propValues?.[disc] ?? []) as unknown[]) {
      if (seen.has(v)) throw new Error(`Duplicate discriminator value: ${String(v)}`);
      seen.add(v);
    }
  }
  return z.discriminatedUnion(disc, options);
}

Prevention

When it happens

Trigger: Two options in z.discriminatedUnion("type", [...]) both declaring z.literal("a") at the "type" key; overlapping z.enum() sets across options that share a value; copy-pasting an option and forgetting to change its discriminator literal; discriminator literals computed dynamically that collide.

Common situations: Refactoring union members by cloning schemas; merging two previously-separate discriminated unions whose literals happen to overlap; enums that grow to include a value already used by another branch; casing issues ("Active" vs "active") that look distinct but collide after normalization elsewhere.

Related errors


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

Appendix: source

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

          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,
          inst,
        });
        return payload;

View on GitHub (pinned to 2d90846af9)