# How to Combine Schemas with Zod's Intersection

> Discover how to combine schemas with Zod's intersection combinator. Learn to merge validators for simultaneous input satisfaction and a compile-time TypeScript intersection type, enhancing your data validation.

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

---

**Zod's `intersection` combinator merges two schemas into a single validator that requires values to satisfy both input schemas simultaneously, producing a TypeScript intersection type at compile time.**

The ability to **combine schemas with Zod's intersection** method enables developers to compose complex, type-safe validators from simple building blocks. Within the `colinhacks/zod` library, this combinator merges multiple constraints into a single schema that validates input values against all constituent types while preserving precise type inference.

## Core Implementation

According to the source code in [`packages/zod/src/v4/classic/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/schemas.ts) (lines 1415-1439), the `intersection` factory function creates a `ZodIntersection` instance with `type: "intersection"` that stores references to both the left and right-hand side schemas. A lightweight version for the minimal runtime exists in [`packages/zod/src/v4/mini/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/mini/schemas.ts) (lines 939-997), maintaining identical structural behavior for tree-shakeable bundles.

When exporting to JSON Schema, the processor located in [`packages/zod/src/v4/core/json-schema-processors.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/json-schema-processors.ts) (lines 56-73) handles intersections by gathering the two child schemas and emitting an `allOf` array, flattening simple intersections where possible to produce valid JSON Schema output.

## Validation Flow and Merging Behavior

The `ZodIntersection.parse` method executes validation in two distinct phases:

1. **Left-side validation** – The input is first parsed against the left-hand schema.
2. **Right-side validation** – The intermediate result is then parsed against the right-hand schema.

For object schemas, Zod merges the property maps from both sides. If both schemas define the same key with incompatible output transformations, the merge fails and throws an `Unmergable intersection` error with a path array pinpointing the offending key, as demonstrated in [`packages/zod/src/v4/classic/tests/intersection.test.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/tests/intersection.test.ts).

The resulting schema inherits the **strip** or **passthrough** behavior from the right-hand side when both inputs are objects. This means `strictObject` + `object` strips unknown keys, while `looseObject` + `object` keeps them, depending on which schema occupies the right-hand position.

## Type Inference

Zod's type system treats intersections as TypeScript's `&` operator. The generic interface `ZodIntersection<A extends core.SomeType, B extends core.SomeType>` declared in the core types ensures that `z.infer<typeof Combined>` resolves to `Infer<A> & Infer<B>`. This provides compile-time guarantees that validated values satisfy the structural requirements of both constituent schemas simultaneously.

## How to Combine Schemas: Practical Examples

### Basic Object Intersection

Combine two object schemas to require properties from both:

```typescript
import * as z from "zod/v4";

const A = z.object({ a: z.string() });
const B = z.object({ b: z.number() });

const AB = z.intersection(A, B);
// Type: { a: string } & { b: number }

AB.parse({ a: "foo", b: 42 });          // ✅ passes
// AB.parse({ a: "foo" });               // ❌ throws validation error

```

### Strict vs. Loose Object Merging

Control unknown key retention through schema ordering:

```typescript
const Loose = z.looseObject({ a: z.string() });
const Strict = z.strictObject({ b: z.number() });

const Mixed = z.intersection(Loose, Strict);
// Result keeps unknown keys (passthrough) because `Loose` is loose
Mixed.parse({ a: "x", b: 1, extra: true }); // ✅ extra retained

const StrictOnly = z.intersection(
  z.strictObject({ a: z.string() }),
  z.strictObject({ b: z.number() })
);
// Unknown keys are stripped (right‑hand side strict)
StrictOnly.parse({ a: "x", b: 1, extra: true }); // ✅ returns { a:"x", b:1 }

```

### Deep Nested Intersections

Merge schemas with nested object structures:

```typescript
const Base = z.object({
  meta: z.object({ created: z.boolean() })
});

const Extension = z.object({
  meta: z.object({ edited: z.boolean() })
});

const Merged = z.intersection(Base, Extension);
// Inferred type:
// { meta: { created: boolean } & { edited: boolean } }

Merged.parse({ meta: { created: true, edited: false } }); // ✅

```

### Handling Incompatible Transformations

Attempting to intersect incompatible transforms throws a runtime error:

```typescript
const Num = z.number();
const NumPlus = z.number().transform(v => v + 1);

const Bad = z.intersection(Num, NumPlus);
// Parsing any number throws:
Bad.parse(5); // ❌ Error: Unmergable intersection. Error path: []

```

### JSON Schema Export

Intersections translate to `allOf` arrays in JSON Schema:

```typescript
const Schema = z.intersection(
  z.object({ id: z.string() }),
  z.object({ name: z.string() })
);

console.log(Schema.toJSON()); 
// → { allOf: [{type:"object", properties: {id: {type: "string"}}}, 
//             {type:"object", properties: {name: {type: "string"}}}] }

```

## Summary

- **ZodIntersection** instances validate inputs against both left and right schemas sequentially, as implemented in [`packages/zod/src/v4/classic/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/schemas.ts)
- Object property maps are merged during validation, with conflicts throwing `Unmergable intersection` errors that include specific path arrays
- Unknown key handling inherits from the right-hand side schema's **strip** or **passthrough** configuration
- Type inference produces TypeScript intersection types (`A & B`) for compile-time safety
- JSON Schema export represents combined constraints using standard `allOf` arrays

## Frequently Asked Questions

### What happens when intersecting object schemas with overlapping keys?

Zod attempts to merge the property maps from both schemas. If both schemas define the same key with incompatible output types or transformations, the merge fails and throws an `Unmergable intersection` error with a specific path array indicating the conflicting key location.

### How does Zod handle unknown keys in intersected objects?

The resulting schema inherits the strip or passthrough behavior from the right-hand side schema when both inputs are objects. If the right-hand schema is strict, unknown keys are removed; if it is loose (passthrough), unknown keys are retained in the final output.

### Can I intersect more than two schemas at once?

The `z.intersection()` function accepts exactly two arguments. To combine three or more schemas, you must nest intersection calls: `z.intersection(z.intersection(A, B), C)`. Each nested intersection creates a new `ZodIntersection` type composing the constraints of all constituent schemas.

### Why does my intersection throw "Unmergable intersection" when parsing?

This error occurs when Zod cannot reconcile the output types of both schemas, such as intersecting two number schemas where one applies a transform (e.g., `.transform(v => v + 1)`). Since the transformed values are incompatible at the type level, Zod throws this error at parse time with a path array indicating where the conflict occurred.