{"id":"b7218630a8958ab0","repo":"colinhacks/zod","slug":"invalid-discriminated-union-option-at-index-def","errorCode":null,"errorMessage":"Invalid discriminated union option at index \"${def.options.indexOf(option)}\"","messagePattern":"Invalid discriminated union option at index \"(.+?)\"","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/zod/src/v4/core/schemas.ts","lineNumber":2373,"sourceCode":"  Disc extends string = string,\n> extends $ZodType {\n  _zod: $ZodDiscriminatedUnionInternals<Options, Disc>;\n}\n\nexport const $ZodDiscriminatedUnion: core.$constructor<$ZodDiscriminatedUnion> =\n  /*@__PURE__*/\n  core.$constructor(\"$ZodDiscriminatedUnion\", (inst, def) => {\n    def.inclusive = false;\n\n    $ZodUnion.init(inst, def);\n\n    const _super = inst._zod.parse;\n    util.defineLazy(inst._zod, \"propValues\", () => {\n      const propValues: util.PropValues = {};\n      for (const option of def.options) {\n        const pv = option._zod.propValues;\n        if (!pv || Object.keys(pv).length === 0)\n          throw new Error(`Invalid discriminated union option at index \"${def.options.indexOf(option)}\"`);\n        for (const [k, v] of Object.entries(pv!)) {\n          if (!propValues[k]) propValues[k] = new Set();\n          for (const val of v) {\n            propValues[k].add(val);\n          }\n        }\n      }\n      return propValues;\n    });\n\n    const disc = util.cached(() => {\n      const opts = def.options as $ZodTypeDiscriminable[];\n      const map: Map<util.Primitive, $ZodType> = new Map();\n      for (const o of opts) {\n        const values = o._zod.propValues?.[def.discriminator];\n        if (!values || values.size === 0)\n          throw new Error(`Invalid discriminated union option at index \"${def.options.indexOf(o)}\"`);\n        for (const v of values) {","sourceCodeStart":2355,"sourceCodeEnd":2391,"githubUrl":"https://github.com/colinhacks/zod/blob/912f0f51b0ced654d0069741e7160834dca742ee/packages/zod/src/v4/core/schemas.ts#L2355-L2391","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst U = z.discriminatedUnion('type', [\n  z.object({ type: z.literal('a'), value: z.string() }),\n  z.string(), // not discriminable\n]);\n\n// after\nconst U = z.discriminatedUnion('type', [\n  z.object({ type: z.literal('a'), value: z.string() }),\n  z.object({ type: z.literal('b'), value: z.number() }),\n]);","handlingStrategy":"validation","validationCode":"import { z } from 'zod';\nfunction assertDiscriminableOptions(discriminator, options) {\n  options.forEach((opt, i) => {\n    const pv = opt._zod?.propValues;\n    if (!pv || Object.keys(pv).length === 0) {\n      throw new Error(`Option ${i} is not discriminable (no literal/enum properties)`);\n    }\n    if (!(discriminator in pv)) {\n      throw new Error(`Option ${i} is missing discriminator \"${discriminator}\"`);\n    }\n  });\n}\n// call before constructing the union:\nassertDiscriminableOptions('type', [optA, optB]);","typeGuard":"import type { z } from 'zod';\nfunction isDiscriminableObject(s: z.ZodType): boolean {\n  const pv = (s as any)._zod?.propValues;\n  return !!pv && Object.keys(pv).length > 0;\n}","tryCatchPattern":"try {\n  const U = z.discriminatedUnion('type', options);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Invalid discriminated union option')) {\n    // log which option failed and fall back to z.union or fix the option\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["discriminated-union","schema-definition","validation"],"analyzedSha":"912f0f51b0ced654d0069741e7160834dca742ee","analyzedAt":"2026-08-03T17:41:55.908Z","schemaVersion":2}