mastra-ai/mastra · error

Union must have at least 2 options

Error message

Union must have at least 2 options

What it means

defaultZodUnionHandler in the Zod v4 compatibility path rebuilds unions from `value._zod.def.options` and requires at least 2 options because z.union() needs a tuple of 2+. This guard throws when a union with fewer than 2 options reaches the handler.

Source

Thrown at packages/schema-compat/src/schema-compatibility-v4.ts:457

    const legacyDescription = value.description;

    const description = this.mergeParameterDescription(metaDescription || legacyDescription, constraints);
    if (description) {
      result = result.describe(description);
    }
    return result;
  }

  /**
   * Default handler for Zod union types. Processes all union options.
   *
   * @param value - The Zod union to process
   * @returns The processed Zod union
   * @throws Error if union has fewer than 2 options
   */
  public defaultZodUnionHandler(value: ZodUnion<[ZodAny, ...ZodAny[]]>): ZodAny {
    const processedOptions = value._zod.def.options.map((option: ZodAny) => this.processZodType(option));
    if (processedOptions.length < 2) throw new Error('Union must have at least 2 options');
    let result = z.union(processedOptions as [ZodAny, ZodAny, ...ZodAny[]]);
    if (value.description) {
      result = result.describe(value.description);
    }
    // @ts-expect-error - fix later
    return result;
  }

  /**
   * Default handler for Zod string types. Processes string validation constraints.
   *
   * @param value - The Zod string to process
   * @param handleChecks - String constraints to convert to descriptions vs keep as validation
   * @returns The processed Zod string
   */
  public defaultZodStringHandler(
    value: ZodString,
    handleChecks: readonly StringCheckType[] = ALL_STRING_CHECKS,

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Provide at least two options in the union: z.union([a, b]).
  2. When only one branch exists, return the branch type itself, unwrapped.
  3. For runtime-built unions, branch on options.length before calling the compat layer.
  4. Use z.optional()/z.nullable() instead of one-sided unions to express optionality.

Example fix

// before
const maybe = z.union([z.string()]);
// after
const maybe = z.union([z.string(), z.null()]); // or z.string().nullable()
Defensive patterns

Strategy: validation

Validate before calling

function toUnionOrSingleV4(types: z.ZodTypeAny[]): z.ZodTypeAny {
  if (types.length === 0) throw new TypeError('Need at least one type');
  return types.length === 1 ? types[0] : z.union(types as [z.ZodTypeAny, z.ZodTypeAny, ...z.ZodTypeAny[]]);
}

Type guard

function isValidUnionV4(u: { _zod: { def: { options: unknown[] } } }): boolean {
  return u._zod.def.options.length >= 2;
}

Try / catch

try {
  processed = compat.process(schema);
} catch (e) {
  if (e.message === 'Union must have at least 2 options') {
    throw new Error('Dynamic union collapsed; ensure >=2 branches or unwrap', { cause: e });
  }
  throw e;
}

Prevention

When it happens

Trigger: A ZodUnion whose definition contains 0 or 1 options after processing — usually from dynamically built unions, or a single-option union created at runtime in Zod v4.

Common situations: Generated schemas (e.g. from OpenAPI/JSON Schema converters) that collapse to one branch; code that spreads optional/filtered option arrays into z.union; version migrations from Zod v3 unions.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/3914132e703ff4a4. Report an issue: GitHub.