# How to Implement Discriminated Unions in Zod: A Complete Guide

> Learn how to implement discriminated unions in Zod with z.discriminatedUnion. Achieve O(1) lookup and precise errors for robust data validation.

- Repository: [Colin McDonnell/zod](https://github.com/colinhacks/zod)
- Tags: how-to-guide
- Published: 2026-02-23

---

**Use `z.discriminatedUnion(discriminator, options)` to create a schema that parses objects by matching a specific key against literal values, enabling O(1) lookup performance and precise error reporting.**

Discriminated unions in Zod provide a type-safe way to handle polymorphic data structures where a single property determines the object's shape. According to the colinhacks/zod source code, this pattern is implemented as a specialized union schema that avoids the linear search overhead of standard unions by building a lookup map at schema construction time.

## What Are Discriminated Unions in Zod?

A **discriminated union** is a schema composition pattern where multiple object schemas share a common property (the discriminator), and each schema defines a unique literal value for that property. When parsing, Zod reads the discriminator value and immediately selects the matching schema rather than trying each option sequentially.

This approach differs from `z.union()` in both performance and error specificity. While standard unions attempt every option until one succeeds, discriminated unions achieve **O(1) lookup time** by pre-computing a map of discriminator values to schemas.

## The Core Architecture

### Factory Functions and Public API

Zod exposes discriminated unions through two public factory functions that delegate to the same core implementation:

- **`z.discriminatedUnion`** in [`packages/zod/src/v4/classic/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/schemas.ts) for the full-featured API
- **`z.discriminatedUnion`** in [`packages/zod/src/v4/mini/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/mini/schemas.ts) for the lightweight "mini" build

Both factories call the internal **`_discriminatedUnion`** constructor defined in [`packages/zod/src/v4/core/api.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/api.ts) at line 1194. This constructor validates that every member schema defines a literal value for the discriminator property and ensures no duplicate values exist across options.

### Internal Implementation Details

The core implementation relies on **`ZodDiscriminatedUnionInternals`** defined in [`packages/zod/src/v4/core/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts) at line 2276. This structure stores:

1. **The definition object** containing the `discriminator` key name and optional `unionFallback` flag
2. **A lazy `propValues` map** that aggregates all possible literal values for every property across all member schemas

When the schema is first used, Zod computes `propValues` by iterating over each option and extracting its literal properties. If any option lacks a literal value for the discriminator, the constructor throws an error immediately.

The parsing logic uses a cached **discriminator map** (`disc` function at line 3015) that maps each primitive discriminator value to its corresponding schema. During `parse()`, Zod extracts the discriminator value from the input, performs a Map lookup, and delegates to the matching schema's parse method. This avoids the linear iteration required by standard unions.

## How to Define Discriminated Unions in Zod

### Basic Object Discriminated Union

Define a discriminated union by providing the discriminator key as the first argument and an array of object schemas as the second:

```typescript
import { z } from "zod";

const Shape = z.discriminatedUnion("type", [
  z.object({ type: z.literal("circle"), radius: z.number() }),
  z.object({ type: z.literal("square"), side: z.number() }),
]);

// Valid parsing
const result = Shape.parse({ type: "circle", radius: 10 });
// → { type: "circle", radius: 10 }

```

This example is verified in [`packages/zod/src/v4/classic/tests/discriminated-unions.test.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/tests/discriminated-unions.test.ts) at line 31.

### Optional Discriminators

Zod supports optional discriminator values, where the schema falls back to sequential matching when the discriminator is absent:

```typescript
const MaybeSquare = z.discriminatedUnion("type", [
  z.object({ type: z.literal("square").optional(), side: z.number() }),
  z.object({ type: z.literal("triangle"), base: z.number(), height: z.number() }),
]);

// Both inputs accepted
MaybeSquare.parse({ side: 5 });                    // Matches first schema (no discriminator)
MaybeSquare.parse({ type: "square", side: 5 });  // Matches via discriminator

```

See the test implementation at line 54 in the discriminated unions test file.

### Primitive Discriminator Values

Discriminators can use any primitive literal type, including booleans and numbers:

```typescript
const Primitive = z.discriminatedUnion("type", [
  z.object({ type: z.literal("true"), value: z.string() }),
  z.object({ type: z.literal(true), value: z.string() }),
  z.object({ type: z.literal(42), value: z.string() }),
]);

Primitive.parse({ type: true, value: "yes" });    // Matches second schema
Primitive.parse({ type: 42, value: "answer" });   // Matches third schema

```

Test coverage for primitive discriminators exists at line 63 in [`packages/zod/src/v4/classic/tests/discriminated-unions.test.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/tests/discriminated-unions.test.ts).

## Parsing and Performance Characteristics

### O(1) Lookup Performance

Unlike `z.union()`, which attempts each option sequentially until one succeeds, discriminated unions in Zod achieve **constant-time lookup** through a pre-computed Map. During schema construction in `_discriminatedUnion`, Zod builds a discriminator map that associates each literal value with its corresponding schema.

When `parse()` is called, the implementation extracts the discriminator value and performs a single Map lookup to select the correct parser. This eliminates the linear iteration overhead present in standard unions, making discriminated unions significantly faster when dealing with many variants.

### Error Handling

When the discriminator value does not match any option, Zod throws a `ZodError` with specific path information pointing to the discriminator key. If the discriminator property is missing from the input, the parser falls back to standard union behavior, attempting each option sequentially until one matches or all fail.

The error messages are generated by the selected branch's parser, providing precise validation feedback for the specific shape that matched the discriminator value.

## Summary

- **Discriminated unions in Zod** use `z.discriminatedUnion(discriminator, options)` to create schemas that select parsers based on a literal property value.
- The implementation resides in [`packages/zod/src/v4/core/api.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/api.ts) (`_discriminatedUnion`) and [`packages/zod/src/v4/core/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts) (`ZodDiscriminatedUnionInternals`).
- **Performance**: O(1) lookup via pre-computed Map versus linear search in standard unions.
- **Flexibility**: Supports string, boolean, and number literals as discriminators, with optional fallback behavior when discriminators are absent.
- **Validation**: Duplicate discriminator values throw errors at schema construction time, ensuring type safety.

## Frequently Asked Questions

### What is the difference between z.union and z.discriminatedUnion?

**`z.union()`** attempts each member schema sequentially until one succeeds, resulting in O(n) performance and potentially vague error messages when multiple schemas could match. **`z.discriminatedUnion()`** uses a discriminator key to perform O(1) Map lookups, immediately selecting the correct schema based on a literal property value. According to the source code in [`packages/zod/src/v4/core/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts), discriminated unions also provide more specific error paths since they know exactly which branch failed.

### Can I use optional discriminators in Zod discriminated unions?

Yes. When a member schema defines the discriminator as optional (e.g., `z.literal("square").optional()`), Zod falls back to standard union behavior for that branch. As implemented in the parsing logic, if the discriminator key is missing from the input, the schema attempts each option sequentially until finding a match. This allows you to handle both discriminated and non-discriminated shapes within the same union, though you lose the O(1) performance guarantee when the discriminator is absent.

### What happens if the discriminator value doesn't match any option?

Zod throws a **`ZodError`** with a specific issue code indicating that no branch matched the discriminator value. The error includes the path to the discriminator property and the invalid value received. According to the test suite in [`packages/zod/src/v4/classic/tests/discriminated-unions.test.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/tests/discriminated-unions.test.ts), this validation occurs during the parsing phase after the discriminator map lookup fails to find a matching key. The error message clearly identifies which discriminator value was expected versus what was received.

### Are discriminated unions in Zod faster than regular unions?

Yes, significantly. **Discriminated unions provide O(1) lookup time** compared to the O(n) linear search of regular unions. As detailed in [`packages/zod/src/v4/core/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts), Zod pre-computes a Map during schema construction that associates each discriminator literal value with its corresponding schema. When parsing, Zod extracts the discriminator value and performs a single Map lookup to select the parser, avoiding the need to attempt each option sequentially. This performance advantage grows linearly with the number of union members, making discriminated unions essential for schemas with many variants.