Capítulo 52 de 61

Chapter 52: Zod Core — Checks, Errors & Issues

Core Idea

Checks are post-parse refinements that never change a schema's inferred type; some check subclasses double as schema types (string formats are both), and every validation failure surfaces as a $ZodIssue inside a $ZodError that — for performance reasons — deliberately does not extend the built-in Error class.

Key Concepts

  • $ZodCheck<T>: base class for checks, with _zod.def.check as the string discriminant ("min_length", "less_than", "string_format", etc.) and _zod.check() holding the validation logic. $ZodChecks is the union of all first-party check subclasses.
  • Discriminating checks: switch on ._zod.def.check; for "string_format" checks, a nested switch on ._zod.def.format further discriminates (email, url, etc.) — $ZodStringFormatChecks is that union.
  • Dual-role classes: some string-format classes (e.g. $ZodEmail) implement both $ZodCheck and $ZodType — usable either as a standalone type (z.email()) or as a check appended via .check() (z.string().check(z.email())). When used as a check, both the schema's own parser and the check's validation run.
  • $ZodError: base error class holding .issues: $ZodIssue[]; it does not extend Error, so instanceof Error is false on it. The zod package's ZodError subclasses $ZodError with extra convenience methods; zod/mini uses $ZodError directly.
  • $ZodIssueBase: every issue has code, input, path, and message. First-party issue subtypes cover the major failure categories: invalid type, too big/small, invalid string format, not a multiple of, unrecognized keys, invalid union, invalid key/element/value, and custom.

Code Examples

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

// discriminating a check, including the nested string-format case
const check = {} as z.$ZodChecks;
switch (check._zod.def.check) {
  case "less_than":
  case "greater_than":
    break;
  case "string_format": {
    const formatCheck = check as z.$ZodStringFormatChecks;
    switch (formatCheck._zod.def.format) {
      case "email":
      case "url":
        break;
    }
  }
}

// the same class used two ways
z.email().parse("user@example.com");            // as a standalone type
z.string().check(z.email()).parse("user@example.com"); // as a check on top of z.string()
  • What it demonstrates: navigating the check discriminant hierarchy, and the dual type/check nature of string-format classes.

Anti-patterns

  • Using instanceof Error to detect a Zod validation failure: $ZodError doesn't extend Error, so this check silently fails — use instanceof z.ZodError (or $ZodError in Zod Mini/Core) instead.

Key Takeaways

  1. Checks are additive and type-inert by design — they can reject a value but never change what TypeScript infers the schema produces.
  2. String-format classes being both types and checks is why z.email() and z.string().check(z.email()) behave identically at runtime — they're the same underlying class used in two different roles.
  3. $ZodError not extending Error is a deliberate performance choice that tooling authors need to account for explicitly.

Connects To

  • Zod Core — Schema Classes & Internals: the parallel _zod.def discrimination pattern used for schema types.
  • Formatting Errors: the higher-level treeifyError/flattenError/prettifyError utilities built on top of $ZodIssue.