Zod logo

@zod/core

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

What's in @zod/core

This package exports the core classes and utilities that are consumed by zod and @zod/mini. It is not intended to be used directly; instead it's designed to be extended by other packages. It implements:

import * as z from "@zod/core";
 
// the base class for all Zod schemas
z.$ZodType;
 
// subclasses of $ZodType that implement common parsers
z.$ZodString
z.$ZodObject
z.$ZodArray
// ...
 
// the base class for all Zod checks
z.$ZodCheck;
 
// subclasses of $ZodCheck that implement common checks
z.$ZodCheckMinLength
z.$ZodCheckMaxLength
 
// the base class for all Zod errors
z.$ZodError;
 
// issue formats (types only)
{} as z.$ZodIssue;
 
// utils
z.util.isValidJWT(...);

For simplicity, the zod and @zod/mini packages re-export their @zod/core dependency as z.core, but you shouldn't rely on this.

import * as z from "zod";
 
z.core.$ZodString;
// the base class for string schemas

Schemas

The base class for all Zod schemas is $ZodType. It accepts two generic parameters: Output and Input.

export class $ZodType<Output = unknown, Input = unknown> {
  _zod: { /* internals */}
}

@zod/core exports a number of subclasses that implement some common parsers. A union of all first-party subclasses is exported as z.$ZodTypes.

export type $ZodTypes =
  | $ZodString
  | $ZodNumber
  | $ZodBigInt
  | $ZodBoolean
  | $ZodDate
  | $ZodSymbol
  | $ZodUndefined
  | $ZodNullable
  | $ZodNull
  | $ZodAny
  | $ZodUnknown
  | $ZodNever
  | $ZodVoid
  | $ZodArray
  | $ZodObject
  | $ZodUnion
  | $ZodIntersection
  | $ZodTuple
  | $ZodRecord
  | $ZodMap
  | $ZodSet
  | $ZodLiteral
  | $ZodEnum
  | $ZodPromise
  | $ZodLazy
  | $ZodOptional
  | $ZodDefault
  | $ZodTemplateLiteral
  | $ZodCustom
  | $ZodTransform
  | $ZodNonOptional
  | $ZodReadonly
  | $ZodNaN
  | $ZodPipe
  | $ZodSuccess
  | $ZodCatch
  | $ZodFile;

All @zod/core subclasses only contain a single property: _zod. This property is an object containing the schemas internals. The goal is to make @zod/core as extensible and unopinionated as possible. Other libraries can "build their own Zod" on top of these classes without @zod/core cluttering up the interface.

Refer to the implementations of zod and @zod/mini for examples of how to extend these classes.

The _zod internals property contains some notable properties:

  • .def — The schema's definition: this is the object you pass into the class's constructor to create an instance. It completely describes the schema, and it's JSON-serializable.
    • .def.type — A string representing the schema's type, e.g. "string", "object", "array", etc.
    • .def.checks — An array of checks that are executed by the schema after parsing.
  • .input — A virtual property that "stores" the schema's inferred input type.
  • .output — A virtual property that "stores" the schema's inferred output type.
  • .run() — The schema's internal parser implementation.

If you are implementing a tool (say, a code generator) that must traverse Zod schemas, you can cast any schema to $ZodTypes and use the def property to discriminate between these classes.

export function walk(_schema: z.$ZodType) {
  const schema = _schema as z.$ZodTypes;
  const def = schema._zod.def;
  switch (def.type) {
    case "string": {
      // ...
      break;
    }
    case "object": {
      // ...
      break;
    }
  }
}

There are a number of subclasses of $ZodString that implement various string formats. These are exported as z.$ZodStringFormatTypes.

export type $ZodStringFormatTypes =
  | $ZodGUID
  | $ZodUUID
  | $ZodEmail
  | $ZodURL
  | $ZodEmoji
  | $ZodNanoID
  | $ZodCUID
  | $ZodCUID2
  | $ZodULID
  | $ZodXID
  | $ZodKSUID
  | $ZodISODateTime
  | $ZodISODate
  | $ZodISOTime
  | $ZodISODuration
  | $ZodIPv4
  | $ZodIPv6
  | $ZodCIDRv4
  | $ZodCIDRv6
  | $ZodBase64
  | $ZodBase64URL
  | $ZodE164
  | $ZodJWT

Checks

Every Zod schema contains an array of checks. These perform post-parsing refinements (and occasionally mutations) that do not affect the inferred type.

const schema = z.string().check(z.email()).check(z.min(5));
// => $ZodString
 
schema._zod.def.checks;
// => [$ZodCheckEmail, $ZodCheckMinLength]

The base class for all Zod checks is $ZodCheck. It accepts a single generic parameter T.

export class $ZodCheck<in T = unknown> {
  _zod: { /* internals */}
}

The _zod internals property contains some notable properties:

  • .def — The check's definition: this is the object you pass into the class's constructor to create the check. It completely describes the check, and it's JSON-serializable.
    • .def.check — A string representing the check's type, e.g. "min_lenth", "less_than", "string_format", etc.
  • .check() — Contains the check's validation logic.

@zod/core exports a number of subclasses that perform some common refinements. All first-party subclasses are exported as a union called z.$ZodChecks.

export type $ZodChecks =
  | $ZodCheckLessThan
  | $ZodCheckGreaterThan
  | $ZodCheckMultipleOf
  | $ZodCheckNumberFormat
  | $ZodCheckBigIntFormat
  | $ZodCheckMaxSize
  | $ZodCheckMinSize
  | $ZodCheckSizeEquals
  | $ZodCheckMaxLength
  | $ZodCheckMinLength
  | $ZodCheckLengthEquals
  | $ZodCheckProperty
  | $ZodCheckMimeType
  | $ZodCheckOverwrite
  | $ZodCheckStringFormat

You can use the ._zod.def.check property to discriminate between these classes.

const check = {} as z.$ZodChecks;
const def = check._zod.def;
 
switch (def.check) {
  case "less_than":
  case "greater_than":
    // ...
    break;
}

As with schema types, there are a number of subclasses of $ZodCheckStringFormat that implement various string formats.

export type $ZodStringFormatChecks =
  | $ZodCheckRegex
  | $ZodCheckLowerCase
  | $ZodCheckUpperCase
  | $ZodCheckIncludes
  | $ZodCheckStartsWith
  | $ZodCheckEndsWith
  | $ZodGUID
  | $ZodUUID
  | $ZodEmail
  | $ZodURL
  | $ZodEmoji
  | $ZodNanoID
  | $ZodCUID
  | $ZodCUID2
  | $ZodULID
  | $ZodXID
  | $ZodKSUID
  | $ZodISODateTime
  | $ZodISODate
  | $ZodISOTime
  | $ZodISODuration
  | $ZodIPv4
  | $ZodIPv6
  | $ZodCIDRv4
  | $ZodCIDRv6
  | $ZodBase64
  | $ZodBase64URL
  | $ZodE164
  | $ZodJWT;

Use a nested switch to discriminate between the different string format checks.

const check = {} as z.$ZodChecks;
const def = check._zod.def;
 
switch (def.check) {
  case "less_than":
  case "greater_than":
  // ...
  case "string_format":
    {
      const formatCheck = check as z.$ZodStringFormatChecks;
      const formatCheckDef = formatCheck._zod.def;
 
      switch (formatCheckDef.format) {
        case "email":
        case "url":
          // do stuff
      }
    }
    break;
}

You'll notice some of these string format checks overlap with the string format types above. That's because these classes implement both the $ZodCheck and $ZodType interfaces. That is, they can be used as either a check or a type. In these cases, both ._zod.parse (the schema parser) and ._zod.check (the check validation) are executed during parsing. In effect, the instance is prepended to its own checks array (though it won't actually exist in ._zod.def.checks).

// as a type
z.email().parse("user@example.com");
 
// as a check
z.string().check(z.email()).parse("user@example.com")

Errors

The base class for all errors in Zod is $ZodError.

For performance reasons, $ZodError does not extend the built-in Error class! So using instanceof Error will return false.

  • The zod package implements a subclass of $ZodError called ZodError with some additional convenience methods.
  • The @zod/mini package directly uses $ZodError
export class $ZodError<T = unknown> implements Error {
 public issues: $ZodIssue[];
}

Issues

The issues property corresponds to an array of $ZodIssue objects. All issues extend the z.$ZodIssueBase interface.

export interface $ZodIssueBase {
  readonly code?: string;
  readonly input?: unknown;
  readonly path: PropertyKey[];
  readonly message: string;
}

Zod defines the following issue subtypes:

export type $ZodIssue =
  | $ZodIssueInvalidType
  | $ZodIssueTooBig
  | $ZodIssueTooSmall
  | $ZodIssueInvalidStringFormat
  | $ZodIssueNotMultipleOf
  | $ZodIssueUnrecognizedKeys
  | $ZodIssueInvalidUnion
  | $ZodIssueInvalidKey
  | $ZodIssueInvalidElement
  | $ZodIssueInvalidValue
  | $ZodIssueCustom;

For details on each type, refer to the implementation.

Building on top of Zod

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

Standard Schema

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.

Peer dependencies

Generally speaking, any library built on top of Zod should include @zod/core in "peerDependencies". This lets your users "bring their own Zod".

// package.json
{
  // ...
  "peerDependencies": {
    "@zod/core": "^0.0.1"
  }
}

When your user installs zod or @zod/mini, their package manager will automatically install @zod/core too. So this peer dependency will be met as long as your user has one of these packages installed.

Versioning

While Zod 4 is in beta, the @zod/core package is published as latest in the v0.x range ("initial development" according to semver). The v1.0.0 release will accompany the first stable release of zod@4.0.

VersionSemver RangeDescription
Beta@zod/core@^0.0.1Versions will say in v0.x range during the beta
Stable@zod/core@^1.0.0It will go stable alongside zod@4. At this point, you'll need to bump the version to ^1.0.0.

If Zod isn't always needed for your library (say, if you are building a framework that supports a number of different schema libraries), use "peerDependenciesMeta" to mark @zod/core as optional.

// package.json
{
  // ...
  "peerDependencies": {
    "@zod/core": "^0.0.1"
  },
  "peerDependenciesMeta": {
    "@zod/core": {
      "optional": true
    }
  }
}

Dev dependencies (local development)

During development, you need to meet your own peer dependency requirement, so you should also add @zod/core to your "devDependencies".

// package.json
{
  "peerDependencies": {
    "@zod/core": "^0.0.1"
  },
  "devDependencies": {
    "@zod/core": "^0.0.1"
  }
}

Future proofing

To future-proof your library, you should always allow for new schema and check classes to be added in the future. If you are using switch statements to discriminate over union types, consider printing a warning when an unknown schema type is encountered.

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
}

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.

On this page