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
- Assign a unique literal value at the discriminator key for each option.
- Audit overlapping z.enum() members and remove duplicates across options.
- If two options legitimately share the same discriminator value, merge them into a single option or use z.union() instead.
- 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
- Assign unique literals at the discriminator key for each option.
- Unit-test the discriminator value set for uniqueness.
- Audit overlapping z.enum() members across options during code review.
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
- Discriminator property
- A discriminator value for key
- Invalid discriminated union option at index
- Invalid discriminated union option at index
- Can't use "invalid_type_error" or "required_error" in…
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)