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); // ❌ throwsExtracting 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" }); // ✅ passesIf 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:
- All the options are object schemas
- 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 123Multiple 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() }),
]);