💎 Zod 4 is now stable!  Read the announcement.
Zod logo

For library authors

Edit this page

This page is primarily intended for consumption by library authors who are building tooling on top of Zod.

If you are a library author and think this page should include some additional guidance, please open an issue!

Zod 3 is functionally end-of-life. It still receives security fixes and unambiguous bug fixes, but no new features will land on [email protected]. For any new library — or any new major version of an existing library — target Zod 4 only. See How to support Zod 3 alongside Zod 4? for the dual-support pattern, intended for maintainers extending existing libraries.

Do I need to depend on Zod?

First things first, make sure you need to depend on Zod at all.

If you're building a library that accepts user-defined schemas to perform black-box validation, you may not need to integrate with Zod specifically. Instead look into Standard Schema. It's a shared interface implemented by most popular validation libraries in the TypeScript ecosystem (see the full list), including Zod.

This spec works great if you accept user-defined schemas and treat them like "black box" validators. Given any compliant library, you can extract inferred input/output types, validate inputs, and get back a standardized error.

If you need Zod specific functionality, read on.

How to configure peer dependencies?

Any library built on top of Zod should include "zod" in "peerDependencies". This lets your users "bring their own Zod".

// package.json
{
  // ...
  "peerDependencies": {
    "zod": "^4.0.0"
  }
}

During development, you need to meet your own peer dependency requirement, so add "zod" to your "devDependencies" as well.

// package.json
{
  "peerDependencies": {
    "zod": "^4.0.0"
  },
  "devDependencies": {
    "zod": "^4.0.0"
  }
}

If you're extending an existing library and wish to keep supporting Zod 3 users, see How to support Zod 3 alongside Zod 4? for the wider peer dependency range.

Which subpaths should I import from?

Import the Zod 4 core package from the "zod/v4/core" subpath:

import * as z4 from "zod/v4/core";

Think of this subpath as a "permalink" to Zod 4. It will remain available forever, even across future major versions of the zod package. The only other permalink subpath is "zod/v3", which you only need if you're also supporting Zod 3.

You generally shouldn't be importing from any other paths. The Zod Core library is a shared library that undergirds both Zod 4 Classic and Zod 4 Mini. It's generally a bad idea to implement any functionality that is specific to one or the other. Do not import from these subpaths:

  • "zod" — ❌ In 3.x releases, this exports Zod 3. In 4.x releases, this exports Zod 4. Use the permalinks instead.
  • "zod/v4" and "zod/v4/mini" — ❌ These subpaths are the homes of Zod 4 Classic and Mini, respectively. If you want your library to work with both Zod and Zod Mini, you should build against the base classes defined in "zod/v4/core". If you reference classes from the "zod/v4" module, your library will not work with Zod Mini, and vice versa. This is extremely discouraged. Use "zod/v4/core" instead, which exports the $-prefixed subclasses that are extended by Zod Classic and Zod Mini. The internals of the classic & mini subclasses are identical; they only differ in which helper methods they implement.

For full context on this versioning approach, see the Versioning in Zod 4 writeup.

How to support Zod 3 alongside Zod 4?

This section is for maintainers of existing libraries with Zod 3 users. For new libraries (or new major versions of existing libraries), support ^4.0.0 only — Zod 3 is functionally end-of-life and won't receive new features.

If you maintain a library with existing Zod 3 users, you can extend it to cover Zod 4 without dropping them. Widen your peer dependency to span both ranges — the "zod/v4" subpath is available starting in 3.25.0:

// package.json
{
  // ...
  "peerDependencies": {
    "zod": "^3.25.0 || ^4.0.0"
  }
}

This doesn't require a new major version of your library. Bumping the peer dependency requires your users to npm upgrade zod, but there were no breaking changes (in fact, no code changes at all) between [email protected] and [email protected], so adding Zod 4 support can land in a minor. If your library is due for a major anyway, cutting one that drops Zod 3 and narrows the peer range to ^4.0.0 is the cleaner option.

Since v3.25.0 the zod package contains copies of both Zod 3 and Zod 4 at their respective subpaths, so you can import both side by side:

import * as z3 from "zod/v3";
import * as z4 from "zod/v4/core";
 
type Schema = z3.ZodTypeAny | z4.$ZodType;
 
function acceptUserSchema(schema: z3.ZodTypeAny | z4.$ZodType) {
  // ...
}

To differentiate between Zod 3 and Zod 4 schemas at runtime, check for the "_zod" property. This property is only defined on Zod 4 schemas.

import type * as z3 from "zod/v3";
import type * as z4 from "zod/v4/core";
 
declare const schema: z3.ZodTypeAny | z4.$ZodType;
 
if ("_zod" in schema) {
  schema._zod.def; // Zod 4 schema
} else {
  schema._def; // Zod 3 schema
}

How to support Zod and Zod Mini simultaneously?

Your library code should only import from "zod/v4/core". This sub-package defines the interfaces, classes, and utilities that are shared between Zod and Zod Mini. As long as you follow this rule, both Zod and Zod Mini will work automatically.

// library code
import * as z4 from "zod/v4/core";
 
export function acceptObjectSchema<T extends z4.$ZodObject>(schema: T){
  // parse data
  z4.parse(schema, { /* somedata */});
  // inspect internals
  schema._zod.def.shape;
}

By building against the shared base interfaces, you can reliably support both sub-packages simultaneously. This function can accept both Zod and Zod Mini schemas.

// user code
import { acceptObjectSchema } from "your-library";
 
// Zod 4
import * as z from "zod";
acceptObjectSchema(z.object({ name: z.string() }));
 
// Zod 4 Mini
import * as zm from "zod/mini";
acceptObjectSchema(zm.object({ name: zm.string() }))

Refer to the Zod Core page for more information on the contents of the core sub-library.

How to accept user-defined schemas?

Accepting user-defined schemas is the a fundamental operation for any library built on Zod. This section outlines the best practices for doing so.

When starting out, it may be tempting to write a function that accepts a Zod schema like this:

import * as z4 from "zod/v4/core";
 
function inferSchema<T>(schema: z4.$ZodType<T>) {
  return schema;
}

This approach is incorrect, and limits TypeScript's ability to properly infer the argument. No matter what you pass in, the type of schema will be an instance of $ZodType.

inferSchema(z.string());
// => $ZodType<string>

This approach loses type information, namely which subclass the input actually is (in this case, ZodString). That means you can't call any string-specific methods like .min() on the result of inferSchema. Instead, your generic parameter should extend the core Zod schema interface:

function inferSchema<T extends z4.$ZodType>(schema: T) {
  return schema;
}
 
inferSchema(z.string());
// => ZodString ✅

To constrain the input schema to a specific subclass:

 
import * as z4 from "zod/v4/core";
 
// only accepts object schemas
function inferSchema<T extends z4.$ZodObject>(schema: T) {
  return schema;
}

To constrain the inferred output type of the input schema:

 
import * as z4 from "zod/v4/core";
 
// only accepts string schemas
function inferSchema<T extends z4.$ZodType<string>>(schema: T) {
  return schema;
}
 
inferSchema(z.string()); // ✅ 
 
inferSchema(z.number()); 
// ❌ The types of '_zod.output' are incompatible between these types. 
// // Type 'number' is not assignable to type 'string'

To parse data with the schema, use the top-level z4.parse/z4.safeParse/z4.parseAsync/z4.safeParseAsync functions. The z4.$ZodType subclass has no methods on it. The usual parsing methods are implemented by Zod and Zod Mini, but are not available in Zod Core.

function parseData<T extends z4.$ZodType>(data: unknown, schema: T): z4.output<T> {
  return z.parse(schema, data);
}
 
parseData("sup", z.string());
// => string

On this page