Skip to Content

Last Updated: 3/12/2026


Unions

Union types (A | B) represent a logical “OR”. Zod union schemas will check the input against each option in order. The first value that validates successfully is returned.

Basic Unions

To create a union schema, use z.union() and pass an array of schemas:

import * as z from "zod"; const stringOrNumber = z.union([z.string(), z.number()]); // string | number stringOrNumber.parse("foo"); // => "foo" stringOrNumber.parse(14); // => 14 stringOrNumber.parse(true); // ❌ throws

Extracting Options

To extract the internal option schemas from a union:

stringOrNumber.options; // [ZodString, ZodNumber]

Exclusive Unions (XOR)

An exclusive union (XOR) is a union where exactly one option must match. Unlike regular unions that succeed when any option matches, z.xor() fails if zero options match OR if multiple options match.

const schema = z.xor([z.string(), z.number()]); schema.parse("hello"); // ✅ passes schema.parse(42); // ✅ passes schema.parse(true); // ❌ fails (zero matches)

This is useful when you want to ensure mutual exclusivity between options:

// Validate that exactly ONE of these matches 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

If the input could match multiple options, z.xor() will fail:

const overlapping = z.xor([z.string(), z.any()]); overlapping.parse("hello"); // ❌ fails (matches both string and any)

Discriminated Unions

A discriminated union  is a special kind of union in which:

  1. All the options are object schemas
  2. They share a particular key (the “discriminator”)

Based on the value of the discriminator key, TypeScript is able to “narrow” the type signature as you’d expect.

type MyResult = | { status: "success"; data: string } | { status: "failed"; error: string }; function handleResult(result: MyResult){ if(result.status === "success"){ result.data; // string } else { result.error; // string } }

You could represent it with a regular z.union(). But regular unions are naive—they check the input against each option in order and return the first one that passes. This can be slow for large unions.

So Zod provides a z.discriminatedUnion() API that uses a discriminator key to make parsing more efficient.

const MyResult = z.discriminatedUnion("status", [ z.object({ status: z.literal("success"), data: z.string() }), z.object({ status: z.literal("failed"), error: z.string() }), ]);

Each option should be an object schema whose discriminator prop (status in the example above) corresponds to some literal value or set of values, usually z.enum(), z.literal(), z.null(), or z.undefined().

When to Use Each Type

  • Regular unions (z.union()): When you need to validate against multiple types and order matters
  • Exclusive unions (z.xor()): When exactly one option must match (mutual exclusivity)
  • Discriminated unions (z.discriminatedUnion()): When validating objects with a common discriminator field for better performance

Common Use Cases

API Response Types

const ApiResponse = z.discriminatedUnion("status", [ z.object({ status: z.literal("success"), data: z.unknown() }), z.object({ status: z.literal("error"), error: z.string() }), ]);

Flexible Input Types

const idSchema = z.union([z.string(), z.number()]); // Accepts both "123" and 123

Multiple Object Shapes

const Pet = z.discriminatedUnion("type", [ z.object({ type: z.literal("dog"), breed: z.string() }), z.object({ type: z.literal("cat"), indoor: z.boolean() }), ]);