colinhacks/zod · error · Error
Invalid discriminated union option at index
Error message
Invalid discriminated union option at index "${def.options.indexOf(option)}" What it means
Thrown inside the lazy propValues initializer of a discriminated union when iterating def.options and an option's _zod.propValues is empty or undefined. propValues are the literal/enum values a schema can produce (used for discriminator detection); an option that contributes none — e.g. a plain z.any(), z.unknown(), z.object({}) with no literal fields, or a schema type that never publishes propValues — cannot participate in discrimination and is rejected when propValues is first accessed.
Solutions
- Ensure every option is an object schema containing a literal (or enum/literals) at the discriminator key.
- Replace any z.any()/z.unknown()/empty-object option with a concrete schema that has the discriminator field.
- If you genuinely need a non-discriminable option, switch from z.discriminatedUnion() to z.union().
- Inspect each option's _zod.propValues in a scratch script to find which one is empty.
Example fix
// before
const U = z.discriminatedUnion("type", [
z.object({ type: z.literal("a"), x: z.number() }),
z.object({ y: z.string() }), // missing discriminator
]);
// 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 buildDiscriminatedUnion(disc: string, options: z.ZodObject[]) {
for (const o of options) {
const pv = (o as any)._zod.propValues;
if (!pv || Object.keys(pv).length === 0) {
throw new Error(`Option is not discriminable (no propValues); use a concrete object schema.`);
}
}
return z.discriminatedUnion(disc, options);
} Type guard
function hasPropValues(schema: unknown): boolean {
const pv = (schema as any)?._zod?.propValues;
return !!pv && Object.keys(pv).length > 0;
} Prevention
- Every discriminated-union option must be an object schema with at least one literal field.
- Never include z.any()/z.unknown()/z.never() as an option.
- If you need a catch-all option, use z.union() instead of z.discriminatedUnion().
When it happens
Trigger: Calling z.discriminatedUnion("type", [SchemaA, z.object({})]) where one option has no discriminable literal fields; including z.any(), z.unknown(), z.never(), a non-literal z.string(), or a passthrough object as an option; wrapping the discriminator field in a transform that strips propValues.
Common situations: Mistakenly mixing a generic catch-all option into a discriminated union; refactoring an option to remove the literal discriminator field; using z.literal-less enums where the discriminator key was renamed in one branch; upgrading from z.union() (which has no discriminator requirement) to z.discriminatedUnion() without ensuring every option has a literal at the discriminator key.
Related errors
- A discriminator value for key
- Discriminator property
- Duplicate discriminator value
- 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/b7218630a8958ab0.
Report an issue: GitHub.
Appendix: source
Thrown at packages/zod/src/v4/core/schemas.ts:2390
Disc extends string = string,
> extends $ZodType {
_zod: $ZodDiscriminatedUnionInternals<Options, Disc>;
}
export const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =
/*@__PURE__*/
core.$constructor("$ZodDiscriminatedUnion", (inst, def) => {
def.inclusive = false;
$ZodUnion.init(inst, def);
const _super = inst._zod.parse;
util.defineLazy(inst._zod, "propValues", () => {
const propValues: util.PropValues = {};
for (const option of def.options) {
const pv = option._zod.propValues;
if (!pv || Object.keys(pv).length === 0)
throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(option)}"`);
for (const [k, v] of Object.entries(pv!)) {
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) {View on GitHub (pinned to 2d90846af9)