How to Combine Schemas with Zod's Intersection
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 (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 (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 (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:
- Left-side validation – The input is first parsed against the left-hand schema.
- 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.
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:
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:
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:
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:
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:
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 - Object property maps are merged during validation, with conflicts throwing
Unmergable intersectionerrors 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
allOfarrays
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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →