mirror of
https://github.com/hanzoai/zod.git
synced 2026-09-24 17:04:49 +00:00
253 lines
9.7 KiB
Plaintext
253 lines
9.7 KiB
Plaintext
---
|
|
title: For library authors
|
|
description: "Guidelines and best practices for library authors integrating with Zod"
|
|
---
|
|
|
|
import { Callout } from "fumadocs-ui/components/callout";
|
|
|
|
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!
|
|
|
|
<Callout>
|
|
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?](#how-to-support-zod-3-alongside-zod-4) for the dual-support pattern, intended for maintainers extending existing libraries.
|
|
</Callout>
|
|
|
|
## 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](https://standardschema.dev/). It's a shared interface implemented by most popular validation libraries in the TypeScript ecosystem (see the [full list](https://standardschema.dev/#what-schema-libraries-implement-the-spec)), 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".
|
|
|
|
```json
|
|
// 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.
|
|
|
|
```ts
|
|
// 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?](#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:
|
|
|
|
```ts
|
|
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](#how-to-support-zod-3-alongside-zod-4).
|
|
|
|
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](https://github.com/colinhacks/zod/issues/4371) writeup.
|
|
|
|
## How to support Zod 3 alongside Zod 4?
|
|
|
|
<Callout>
|
|
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.
|
|
</Callout>
|
|
|
|
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`:
|
|
|
|
```json
|
|
// 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:
|
|
|
|
```ts
|
|
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.
|
|
|
|
```ts
|
|
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.
|
|
|
|
```ts
|
|
// 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.
|
|
|
|
```ts
|
|
// 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](/packages/core) page for more information on the contents of the core sub-library.
|
|
|
|
{/* ### Future proofing
|
|
|
|
To future-proof your library, your code should always allow for new schema and check classes to be added in the future. The addition of a new schema type is *not* considered a breaking change.
|
|
|
|
One common pattern when introspecting Zod schemas is to write a switch statement over the set of first-party schema types:
|
|
|
|
```ts
|
|
const schema = {} as z.$ZodTypes;
|
|
const def = schema._zod.def;
|
|
switch (def.type) {
|
|
case "string":
|
|
// ...
|
|
break;
|
|
case "object":
|
|
// ...
|
|
break;
|
|
default:
|
|
console.warn(`Unknown schema type: ${def.type}`);
|
|
// reasonable fallback behavior
|
|
}
|
|
```
|
|
|
|
To future-proof this code, your `default` case should probably not throw an error. Instead, it should print an informative error and fall back to some reasonable behavior. If instead you `throw` an error in the default case, your library will be unusable if/when new schemas types are added in the future. Best to print a warning and treat it as a "no-op" (or some other reasonable fallback behavior). The same applies to unrecognized check types, string formats, etc.
|
|
|
|
*/}
|
|
|
|
## 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:
|
|
|
|
```ts
|
|
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`.
|
|
|
|
```ts
|
|
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:
|
|
|
|
```ts
|
|
function inferSchema<T extends z4.$ZodType>(schema: T) {
|
|
return schema;
|
|
}
|
|
|
|
inferSchema(z.string());
|
|
// => ZodString ✅
|
|
```
|
|
|
|
To constrain the input schema to a specific subclass:
|
|
|
|
```ts
|
|
|
|
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:
|
|
|
|
```ts
|
|
|
|
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.
|
|
|
|
```ts
|
|
function parseData<T extends z4.$ZodType>(data: unknown, schema: T): z4.output<T> {
|
|
return z.parse(schema, data);
|
|
}
|
|
|
|
parseData("sup", z.string());
|
|
// => string
|
|
```
|