colinhacks/zod · error · Error
Invalid discriminated union option at index "${def.options.i
Error message
Invalid discriminated union option at index "${def.options.indexOf(option)}" What it means
Thrown during construction of a discriminated union while lazily computing each option's `propValues` (the set of concrete values its literal/enum fields can take). A union option was supplied that has no `propValues` at all — meaning it is not an object schema with at least one discriminable (literal/enum) property. Every option in a discriminated union must be a discriminable object so the discriminator can be resolved. This fires at definition time (when `propValues` is first accessed, typically during first parse or toJSONSchema).
Source
Thrown at packages/zod/src/v4/core/schemas.ts:2373
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 912f0f51b0)
Solutions
- Inspect the option at the reported index and ensure it is a `z.object({...})` with at least one property defined as `z.literal(...)`, `z.enum([...])`, or a set of `z.literal` unions.
- If a branch is genuinely a non-object/primitive, it cannot participate in a discriminated union — switch to `z.union([...])` instead, or restructure the data so every branch is an object carrying the discriminator.
- Add a literal discriminator field (e.g. `type: z.literal('foo')`) to the offending option so its propValues become non-empty.
Example fix
// before
const U = z.discriminatedUnion('type', [
z.object({ type: z.literal('a'), value: z.string() }),
z.string(), // not discriminable
]);
// after
const U = z.discriminatedUnion('type', [
z.object({ type: z.literal('a'), value: z.string() }),
z.object({ type: z.literal('b'), value: z.number() }),
]); Defensive patterns
Strategy: validation
Validate before calling
import { z } from 'zod';
function assertDiscriminableOptions(discriminator, options) {
options.forEach((opt, i) => {
const pv = opt._zod?.propValues;
if (!pv || Object.keys(pv).length === 0) {
throw new Error(`Option ${i} is not discriminable (no literal/enum properties)`);
}
if (!(discriminator in pv)) {
throw new Error(`Option ${i} is missing discriminator "${discriminator}"`);
}
});
}
// call before constructing the union:
assertDiscriminableOptions('type', [optA, optB]); Type guard
import type { z } from 'zod';
function isDiscriminableObject(s: z.ZodType): boolean {
const pv = (s as any)._zod?.propValues;
return !!pv && Object.keys(pv).length > 0;
} Try / catch
try {
const U = z.discriminatedUnion('type', options);
} catch (e) {
if (e instanceof Error && e.message.startsWith('Invalid discriminated union option')) {
// log which option failed and fall back to z.union or fix the option
}
throw e;
} Prevention
- Always define each discriminated-union option as a z.object with a literal/enum discriminator field.
- Write a unit test that constructs the union at module load so definition-time errors surface immediately.
- Centralize option definitions in one module to keep the discriminator key consistent.
When it happens
Trigger: Passing a non-object schema (e.g. `z.string()`, `z.number()`, `z.any()`) as an option to `z.discriminatedUnion('type', [...])`, or an object schema whose properties are all non-literal/non-enum (so no propValues are collected). Also triggered by `z.never()` or empty `z.object({})` as an option.
Common situations: Migrating a plain `z.union([...])` to `z.discriminatedUnion(...)` where one branch was a primitive; refactoring shared options out and forgetting one branch still needs a literal discriminator field; tests that reuse a generic `z.object({})` placeholder.
Related errors
- Invalid discriminated union option at index "${def.options.i
- Duplicate discriminator value "${String(v)}"
- Cannot create literal schema with no valid values
- Invalid template literal part: ${part}
- A discriminator value for key `${discriminator}` could not b
AI-assisted analysis of colinhacks/zod@912f0f51b0 (2026-08-03).
Data as JSON: /data/errors/b7218630a8958ab0.json.
Report an issue: GitHub.