docs
guides
reference
Combine multiple schemas using union, discriminated union, exclusive union (xor), and intersection operations for flexible type composition.
Creates a schema that validates against one of multiple possible schemas.
/**
* Create a union schema that matches any of the provided schemas
* @param schemas - Array of schemas (at least 2 required)
* @returns Union schema that validates against any option
*/
function union<T extends [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]>(
schemas: T
): ZodUnion<T>;Usage Examples:
import * as z from 'zod';
// Simple union
const StringOrNumber = z.union([z.string(), z.number()]);
type StringOrNumber = z.infer<typeof StringOrNumber>; // string | number
StringOrNumber.parse('hello'); // => "hello"
StringOrNumber.parse(42); // => 42
// Multiple types
const MultiType = z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
]);
type MultiType = z.infer<typeof MultiType>; // string | number | boolean | null
// Complex union
const Result = z.union([
z.object({ success: z.literal(true), data: z.any() }),
z.object({ success: z.literal(false), error: z.string() }),
]);
// Access union options
const options = StringOrNumber.options; // [ZodString, ZodNumber]Unions can also be created using the .or() method:
import * as z from 'zod';
const StringOrNumber = z.string().or(z.number());
const StringOrNumberOrBoolean = z.string().or(z.number()).or(z.boolean());
// Equivalent to:
const Same = z.union([z.string(), z.number(), z.boolean()]);Creates a union optimized for discriminated union types with a shared discriminator key for better performance and error messages.
/**
* Create a discriminated union schema with shared discriminator
* @param discriminator - The discriminator key name
* @param options - Array of object schemas with literal discriminator values
* @returns Discriminated union schema with optimized validation
*/
function discriminatedUnion<
Discriminator extends string,
Options extends ZodDiscriminatedUnionOption<Discriminator>[]
>(
discriminator: Discriminator,
options: Options
): ZodDiscriminatedUnion<Discriminator, Options>;
type ZodDiscriminatedUnionOption<Discriminator extends string> = ZodObject<
{ [K in Discriminator]: ZodLiteral<any> } & ZodRawShape
>;Usage Examples:
import * as z from 'zod';
// Shape discriminated by 'kind'
const Shape = z.discriminatedUnion('kind', [
z.object({
kind: z.literal('circle'),
radius: z.number(),
}),
z.object({
kind: z.literal('rectangle'),
width: z.number(),
height: z.number(),
}),
z.object({
kind: z.literal('triangle'),
base: z.number(),
height: z.number(),
}),
]);
type Shape = z.infer<typeof Shape>;
// { kind: "circle"; radius: number } |
// { kind: "rectangle"; width: number; height: number } |
// { kind: "triangle"; base: number; height: number }
Shape.parse({ kind: 'circle', radius: 5 }); // => valid
Shape.parse({ kind: 'rectangle', width: 10, height: 20 }); // => valid
// API response pattern
const APIResponse = z.discriminatedUnion('status', [
z.object({
status: z.literal('success'),
data: z.any(),
timestamp: z.string(),
}),
z.object({
status: z.literal('error'),
error: z.string(),
code: z.number(),
}),
z.object({
status: z.literal('pending'),
requestId: z.string(),
}),
]);
// Event pattern
const Event = z.discriminatedUnion('type', [
z.object({
type: z.literal('click'),
x: z.number(),
y: z.number(),
}),
z.object({
type: z.literal('keypress'),
key: z.string(),
}),
]);Creates a schema that validates against exactly one of the provided schemas (mutual exclusion).
/**
* Create an exclusive union that matches exactly one schema
* @param schemas - Array of schemas (at least 2 required)
* @returns XOR schema that validates against exactly one option
*/
function xor<T extends [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]>(
schemas: T
): ZodXor<T>;Usage Examples:
import * as z from 'zod';
// Exclusive choice between authentication methods
const AuthMethod = z.xor([
z.object({ username: z.string(), password: z.string() }),
z.object({ token: z.string() }),
z.object({ apiKey: z.string() }),
]);
// Valid: exactly one method
AuthMethod.parse({ username: 'user', password: 'pass' }); // => valid
AuthMethod.parse({ token: 'abc123' }); // => valid
// Invalid: multiple methods
// AuthMethod.parse({ username: 'user', password: 'pass', token: 'abc' }); // throws error
// Invalid: no methods
// AuthMethod.parse({}); // throws error
// Configuration with mutually exclusive options
const Config = z.xor([
z.object({ mode: z.literal('development'), debugLevel: z.number() }),
z.object({ mode: z.literal('production'), optimizationLevel: z.number() }),
]);Creates a schema that validates against both of two schemas (AND operation).
/**
* Create an intersection schema that validates against both schemas
* @param left - First schema
* @param right - Second schema
* @returns Intersection schema requiring both to validate
*/
function intersection<A extends ZodTypeAny, B extends ZodTypeAny>(
left: A,
right: B
): ZodIntersection<A, B>;Usage Examples:
import * as z from 'zod';
// Merge object types
const HasId = z.object({ id: z.string() });
const HasTimestamps = z.object({
createdAt: z.date(),
updatedAt: z.date(),
});
const Entity = z.intersection(HasId, HasTimestamps);
type Entity = z.infer<typeof Entity>;
// { id: string; createdAt: Date; updatedAt: Date }
Entity.parse({
id: '123',
createdAt: new Date(),
updatedAt: new Date(),
}); // => valid
// Combine constraints
const PositiveNumber = z.number().positive();
const Integer = z.number().int();
const PositiveInteger = z.intersection(PositiveNumber, Integer);
// Using .and() method (alternative syntax)
const SameAsAbove = HasId.and(HasTimestamps);
// Multiple intersections
const User = z.object({ name: z.string() });
const Timestamps = z.object({ createdAt: z.date(), updatedAt: z.date() });
const Versioned = z.object({ version: z.number() });
const VersionedUser = User.and(Timestamps).and(Versioned);
type VersionedUser = z.infer<typeof VersionedUser>;
// { name: string; createdAt: Date; updatedAt: Date; version: number }Intersections can be created using the .and() method:
import * as z from 'zod';
const Person = z.object({ name: z.string() });
const Employee = z.object({ employeeId: z.string() });
const PersonEmployee = Person.and(Employee);
// Equivalent to:
const Same = z.intersection(Person, Employee);Common pattern for creating discriminated unions using literal types:
import * as z from 'zod';
// Status enum using union of literals
const Status = z.union([
z.literal('pending'),
z.literal('active'),
z.literal('completed'),
z.literal('cancelled'),
]);
// Better alternative: use z.enum()
const BetterStatus = z.enum(['pending', 'active', 'completed', 'cancelled']);
// Complex discriminated type
const Action = z.discriminatedUnion('type', [
z.object({
type: z.literal('CREATE'),
payload: z.object({ name: z.string() }),
}),
z.object({
type: z.literal('UPDATE'),
payload: z.object({ id: z.string(), name: z.string() }),
}),
z.object({
type: z.literal('DELETE'),
payload: z.object({ id: z.string() }),
}),
]);Complex compositions combining unions and intersections:
import * as z from 'zod';
// Union of intersections
const BaseEntity = z.object({ id: z.string() });
const Timestamps = z.object({ createdAt: z.date(), updatedAt: z.date() });
const User = z.intersection(
BaseEntity,
z.object({ type: z.literal('user'), name: z.string() })
);
const Admin = z.intersection(
BaseEntity,
z.object({ type: z.literal('admin'), name: z.string(), permissions: z.array(z.string()) })
);
const Account = z.union([User, Admin]).and(Timestamps);
// Intersection of unions (less common)
const StringOrNumber = z.union([z.string(), z.number()]);
const NumberOrBoolean = z.union([z.number(), z.boolean()]);
const OnlyNumber = z.intersection(StringOrNumber, NumberOrBoolean);
// This effectively results in z.number() since it's the only common typeUsing parsed unions with TypeScript type guards:
import * as z from 'zod';
const Response = z.union([
z.object({ success: z.literal(true), data: z.any() }),
z.object({ success: z.literal(false), error: z.string() }),
]);
type Response = z.infer<typeof Response>;
function handleResponse(response: Response) {
if (response.success) {
// TypeScript knows this is the success case
console.log(response.data);
} else {
// TypeScript knows this is the error case
console.error(response.error);
}
}
// With discriminated union
const Shape = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('circle'), radius: z.number() }),
z.object({ kind: z.literal('rectangle'), width: z.number(), height: z.number() }),
]);
type Shape = z.infer<typeof Shape>;
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
}
}Union schemas provide access to their options:
interface ZodUnion<T extends [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]> {
readonly options: T;
parse(data: unknown): T[number]['_output'];
safeParse(data: unknown): SafeParseReturnType<unknown, T[number]['_output']>;
// Can use .or() to add more options
or<U extends ZodTypeAny>(schema: U): ZodUnion<[...T, U]>;
}Intersection schemas can be further intersected:
interface ZodIntersection<A extends ZodTypeAny, B extends ZodTypeAny> {
readonly left: A;
readonly right: B;
parse(data: unknown): A['_output'] & B['_output'];
safeParse(data: unknown): SafeParseReturnType<unknown, A['_output'] & B['_output']>;
// Can use .and() to add more intersections
and<U extends ZodTypeAny>(schema: U): ZodIntersection<ZodIntersection<A, B>, U>;
}type ZodTypeAny = ZodType<any, any, any>;
type ZodRawShape = { [k: string]: ZodTypeAny };
class ZodUnion<T extends [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]> {
readonly _type: 'ZodUnion';
readonly options: T;
}
class ZodDiscriminatedUnion<
Discriminator extends string,
Options extends ZodDiscriminatedUnionOption<Discriminator>[]
> {
readonly _type: 'ZodDiscriminatedUnion';
readonly discriminator: Discriminator;
readonly options: Options;
}
class ZodXor<T extends [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]> {
readonly _type: 'ZodXor';
readonly options: T;
}
class ZodIntersection<A extends ZodTypeAny, B extends ZodTypeAny> {
readonly _type: 'ZodIntersection';
readonly left: A;
readonly right: B;
}
type SafeParseReturnType<Input, Output> =
| { success: true; data: Output }
| { success: false; error: ZodError<Input> };
class ZodError<T = any> extends Error {
issues: ZodIssue[];
}