Capítulo 19 de 61

Chapter 19: Unions & Exclusive Unions (XOR)

Core Idea

z.union() passes if any option matches (checked in order, first success wins); z.xor() passes only if exactly one option matches, failing on both zero and multiple matches.

Key Concepts

  • z.union([...]): logical OR — validates against each option in array order, returns the first successful parse.
  • .options (Zod) / .def.options (Zod Mini): the array of option schemas.
  • z.xor([...]): passes only when exactly one option matches; fails with inclusive: false and a matches array of the indices that matched, if more than one option succeeds.
  • Object overlap trap for z.xor(): because z.object() strips unknown keys by default, two object options with overlapping-but-different shapes can both "match" the same input (each just strips what it doesn't recognize) — use .strict() on the narrower option to force true mutual exclusivity.

Code Examples

const stringOrNumber = z.union([z.string(), z.number()]);
stringOrNumber.parse("foo"); // passes
stringOrNumber.parse(14);    // passes

const payment = z.xor([
  z.object({ type: z.literal("card"), cardNumber: z.string() }),
  z.object({ type: z.literal("bank"), accountNumber: z.string() }),
]);
payment.parse({ type: "card", cardNumber: "1234" }); // passes — exactly one option matches

// the overlap trap
const name = z.object({ name: z.string() });
const version = name.extend({ version: z.string() });
z.xor([name, version]).parse({ name: "zod", version: "4" });          // fails — matches both
z.xor([name.strict(), version]).parse({ name: "zod", version: "4" }); // passes
  • What it demonstrates: z.union()'s first-match-wins behavior versus z.xor()'s exactly-one-match requirement, and the strict-object fix for accidental overlap.

Anti-patterns

  • Using z.xor() with plain (non-strict) object options that share a prefix of fields: they'll silently overlap because unknown keys get stripped rather than causing a mismatch — the loose object doesn't actually reject the extra key.

Key Takeaways

  1. z.union() is "at least one matches"; z.xor() is "exactly one matches" — don't reach for xor unless mutual exclusivity is a real requirement.
  2. Union option order matters for z.union() since it short-circuits on first success; for genuinely exclusive shapes, prefer z.discriminatedUnion() for both correctness and performance.
  3. When using z.xor() with object schemas, apply .strict() to prevent looser options from silently matching inputs meant for a stricter one.

Connects To

  • Discriminated unions: a faster, more precise alternative to z.union() when all options share a literal discriminant key.
  • Objects — Definition, Strictness & Shape: .strict()/z.strictObject(), the fix for z.xor() overlap.