colinhacks/zod · error · Error

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

Error message

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

What it means

Thrown while building the discriminator map in `$ZodDiscriminatedUnion`. Two different options register the same discrete value for the discriminator key, so the parser could not decide which option to select. Discriminated unions require each option's discriminator value(s) to be unique across the union.

Source

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

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

Solutions

  1. Look at the duplicate value in the message and find which two options both permit it; change one of them to a distinct literal/enum value.
  2. If overlap is intentional, discriminated union is the wrong tool — use `z.union([...])` and disambiguate with `.refine()` instead.
  3. Audit each option's discriminator field (especially enum/unions of literals) to ensure the sets are disjoint.

Example fix

// before
const U = z.discriminatedUnion('type', [
  z.object({ type: z.literal('admin'), perms: z.array(z.string()) }),
  z.object({ type: z.literal('admin'), scope: z.string() }), // duplicate 'admin'
]);

// after
const U = z.discriminatedUnion('type', [
  z.object({ type: z.literal('admin'), perms: z.array(z.string()) }),
  z.object({ type: z.literal('superadmin'), scope: z.string() }),
]);
Defensive patterns

Strategy: validation

Validate before calling

function assertUniqueDiscriminatorValues(discriminator, options) {
  const seen = new Map();
  options.forEach((opt, i) => {
    const vals = opt._zod?.propValues?.[discriminator] ?? new Set();
    for (const v of vals) {
      if (seen.has(v)) throw new Error(`Duplicate discriminator "${String(v)}" at options ${seen.get(v)} and ${i}`);
      seen.set(v, i);
    }
  });
}

Try / catch

try {
  const U = z.discriminatedUnion('type', options);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('Duplicate discriminator value')) {
    // parse out the value, locate the two colliding options, make them distinct
  }
  throw e;
}

Prevention

When it happens

Trigger: Two options both declaring `type: z.literal('user')`, or two enum-backed discriminator fields whose allowed values overlap (e.g. one `z.enum(['a','b'])` and another `z.enum(['b','c'])` where the shared value is 'b').

Common situations: Adding a new variant and accidentally reusing an existing discriminator value; using unions of literals for the discriminator that overlap between options; merging two separately-defined unions that happen to share a value.

Related errors


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