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
- Give each option a unique discriminator literal value — the duplicate value is named verbatim in the error message.
- 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.
- Audit z.enum and multi-literal discriminators: each literal across all options must appear in exactly one option.
- 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
- Assign each branch a distinct literal value for the discriminator.
- When sharing a base object, override the discriminator literal in each branch.
- If using z.enum as the discriminator, ensure the enum sets across branches are disjoint.
- Add a unit test that constructs the union so duplicate-value errors surface at test time, not runtime.
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
- Duplicate discriminator value
- 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/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)