3. Checks, Refinements, Pipelines & Bidirectional Codecs

Zod v4 unifies post-type validation constraints and in-place value mutations under the composable $ZodCheck<T> descriptor protocol (inst._zod.def.checks), while multi-stage type transformations and bidirectional serialization are modeled via ZodPipe<A, B>, ZodTransform<O, I>, and ZodCodec<A, B>.

03_checks_refinements_codecs_1
Architecture of $ZodCheck Descriptors, Refinements, Pipelines, and Bidirectional Codecs
Source Code
from drawlib import canvas
from drawlib.diagrams.architecture import ArchitectureDiagram, Node, NodeGroup, PhosphorIcon
from drawlib.styles import Styles

canvas.clear()
canvas.setup(width=188, height=96, dpi=150)

d = ArchitectureDiagram(
    node_style=Styles.Primary,
    node_text_style=Styles.DarkBold.patch(text_size=10.0),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.Dark.patch(text_size=10.0),
    node_card_style=Styles.Neutral,
    title="$ZodCheck Descriptors, Refinements & ZodPipe / ZodCodec Topology",
)

# Left group: $ZodCheck Descriptors (bottom-left at (0, 0), node width=46 -> center x = 5 + 23 = 28)
checks_grp = d.add(
    NodeGroup(title="1. $ZodCheck<T> Descriptors", padding=5.0),
    xy=(0.0, 0.0),
)
chk_bounds = checks_grp.add(
    Node("Bounds & Size Checks\nminLength / maxLength / length\ngt / gte / lt / lte / multipleOf", width=46, height=17, icon=PhosphorIcon.RULER, icon_size=6.0, card_style=Styles.Neutral),
    xy=(28.0, 56.0),
)
chk_format = checks_grp.add(
    Node("Format & Property Checks\nregex / lowercase / uppercase\nstartsWith / property / mime", width=46, height=17, icon=PhosphorIcon.MAGNIFYING_GLASS, icon_size=6.0, card_style=Styles.Neutral),
    xy=(28.0, 34.5),
)
chk_mut = checks_grp.add(
    Node("In-Place Overwrite Checks\ntrim / toLowerCase / toUpperCase\nnormalize / slugify / overwrite(fn)", width=46, height=17, icon=PhosphorIcon.MAGIC_WAND, icon_size=6.0, card_style=Styles.PrimaryNeutral),
    xy=(28.0, 13.0),
)

# Middle group: Schema Attachment & Refinements (node width=36 -> center x = 5 + 18 = 23)
attach_grp = d.add(
    NodeGroup(title="2. Schema & Refinements", padding=5.0),
    xy=(66.0, 0.0),
)
schema_node = attach_grp.add(
    Node("ZodType<O, I>\n.check(...checks)\n.with(...checks)", width=36, height=20, icon=PhosphorIcon.CUBE, icon_size=6.5, style=Styles.WhiteBold, card_style=Styles.PrimaryFlat, text_style=Styles.WhiteBold.patch(text_size=10.5)),
    xy=(23.0, 52.0),
)
refine_node = attach_grp.add(
    Node("Custom Refinements\n.refine(fn, params)\n.superRefine(fn)", width=36, height=20, icon=PhosphorIcon.SHIELD_CHECK, icon_size=6.5, card_style=Styles.PrimaryNeutral),
    xy=(23.0, 15.0),
)

# Right group: Pipelines & Bidirectional Codecs (node width=48 -> center x = 5 + 24 = 29)
pipe_grp = d.add(
    NodeGroup(title="3. Pipelines & Codecs", padding=5.0),
    xy=(122.0, 0.0),
)
pipe_node = pipe_grp.add(
    Node("ZodPipe & ZodTransform\n.pipe(target) / .transform(fn)\nz.preprocess(fn, schema)", width=48, height=20, icon=PhosphorIcon.ARROWS_SPLIT, icon_size=6.5, card_style=Styles.Neutral),
    xy=(29.0, 52.0),
)
codec_node = pipe_grp.add(
    Node("ZodCodec<In, Out>\nz.codec(in, out, {decode, encode})\nz.invertCodec(codec)", width=48, height=20, icon=PhosphorIcon.ARROWS_LEFT_RIGHT, icon_size=6.5, style=Styles.WhiteBold, card_style=Styles.AccentFlat, text_style=Styles.WhiteBold.patch(text_size=10.0)),
    xy=(29.0, 15.0),
)

# Connections
d.connect(chk_bounds, schema_node, padding=1.5)
d.connect(chk_format, schema_node, padding=1.5)
d.connect(chk_mut, schema_node, padding=1.5)
d.connect(refine_node, schema_node, label="attaches", padding=1.5)
schema_node.fork([pipe_node, codec_node], at_x=117.0, padding=1.5)

d.draw(xy=(94.0, 46.0), align="center")
canvas.save()

methodZodType.check / refine / superRefine#

Every ZodType (and ZodMiniType) instance stores an immutable array of validation and mutation descriptors in schema.def.checks. Calling .check(), .with(), .refine(), or .superRefine() clones the schema and appends new $ZodCheck instances without mutating the receiver.

Method Signatures#

Method / Signature Return Type Description
schema.check(...checks: (CheckFn<O> | $ZodCheck<O>)[]) this Clones the schema and appends one or more $ZodCheck instances or raw CheckFn<O> callbacks ((payload: ParsePayload<O>) => MaybeAsync<void>).
schema.with(...checks: (CheckFn<O> | $ZodCheck<O>)[]) this Alias for schema.check(...) (identical function reference on prototype).
schema.refine<Ch>(check: Ch, params?: string | $ZodCustomParams) this (or narrowed this & ZodType<R, I>) Appends a boolean or type-predicate refinement check. When check is a TypeScript type guard (val): val is R, narrows the output type to R.
schema.superRefine(fn: (arg: O, ctx: RefinementCtx<O>) => MaybeAsync<void>, params?: $ZodSuperRefineParams) this Appends a multi-issue refinement check that can push arbitrary issues via ctx.addIssue({ code, message, path, ... }).

Refinement Parameters ($ZodCustomParams)#

Option Type Default Description
error string | $ZodErrorMap<$ZodIssueCustom> undefined Custom error message string or function (issue) => string invoked when the refinement fails.
path PropertyKey[] [] Relative property path appended to the issue's path (useful for cross-field object refinements).
abort boolean false When true, sets continue: false on the emitted issue so downstream checks on the same schema are skipped.
when (payload: ParsePayload) => boolean undefined Predicate controlling whether the check runs even if earlier checks produced non-aborting issues.
params Record<string, any> undefined Arbitrary key-value dictionary attached to $ZodIssueCustom.params for downstream i18n or UI inspection.
import * as z from "zod";

const PasswordConfirmSchema = z
  .object({
    password: z.string().min(12),
    confirm: z.string(),
  })
  .refine((data) => data.password === data.confirm, {
    error: "Passwords do not match",
    path: ["confirm"],
  });

const AuditLogSchema = z.string().superRefine((val, ctx) => {
  if (!val.startsWith("EVT_")) {
    ctx.addIssue({
      code: "custom",
      message: "Must start with EVT_ prefix",
      params: { severity: "error" },
    });
  }
  if (val.length > 64) {
    ctx.addIssue({
      code: "too_big",
      origin: "string",
      maximum: 64,
      inclusive: true,
      message: "Event code exceeds 64 characters",
    });
  }
});

functionz.check & Check Factories#

Top-level check factory functions create standalone $ZodCheck<T> descriptors that can be shared across multiple schemas in both zod and zod/mini via schema.check(...).

Standalone Check Factory Catalog#

Factory Function Target Types Emitted Issue Code Description
z.check<O>(fn: CheckFn<O>) Any O Custom (payload.issues.push(...)) Wraps a low-level (payload: ParsePayload<O>) => MaybeAsync<void> callback into a $ZodCheck<O>.
z.refine<T>(fn, params?) Any T "custom" Standalone factory equivalent of .refine(fn, params).
z.superRefine<T>(fn, params?) Any T Any $ZodIssueCode Standalone factory equivalent of .superRefine(fn, params).
z.lt(value, params?) / z.lte(value, params?) number | bigint | Date "too_big" Validates input < value (inclusive: false) or input <= value (inclusive: true).
z.gt(value, params?) / z.gte(value, params?) number | bigint | Date "too_small" Validates input > value (inclusive: false) or input >= value (inclusive: true).
z.positive(params?) / z.negative(params?) number | bigint "too_small" / "too_big" Shorthand for z.gt(0) and z.lt(0).
z.nonnegative(params?) / z.nonpositive(params?) number | bigint "too_small" / "too_big" Shorthand for z.gte(0) and z.lte(0).
z.multipleOf(divisor, params?) number | bigint "not_multiple_of" Validates exact divisibility using float-safe remainder arithmetic or BigInt modulo.
z.minLength(min, params?) / z.maxLength(max, params?) / z.length(len, params?) { length: number } (string, Array, File) "too_small" / "too_big" Validates .length bounds (exact: true on z.length(len)).
z.minSize(min, params?) / z.maxSize(max, params?) / z.size(len, params?) { size: number } (Map, Set, File) "too_small" / "too_big" Validates .size bounds (exact: true on z.size(len)).
z.regex(pattern, params?) string "invalid_format" (format: "regex") Validates string against a RegExp.
z.lowercase(params?) / z.uppercase(params?) string "invalid_format" Validates that a string contains no uppercase or no lowercase characters respectively.
z.includes(substr, params?) / z.startsWith(prefix, params?) / z.endsWith(suffix, params?) string "invalid_format" Validates substring containment, prefix, or suffix.
z.property(prop, schema, params?) Any object/value Delegated from schema Validates input[prop] against schema (e.g., validating .length or custom getter properties).
z.properties(shape, params?) Class instance Delegated from shape[k] Validates multiple instance properties on z.instanceof(Cls) schemas.
z.mime(types: string | string[], params?) File "invalid_value" Validates file.type against an allowed list of MIME type strings.
z.overwrite(fn: (val: T) => T) Any T (None — mutates payload.value) Mutates payload.value = fn(payload.value) in place without changing the schema's static type.
z.trim() / z.toLowerCase() / z.toUpperCase() / z.normalize(form?) / z.slugify() string (None — mutates payload.value) Built-in $ZodCheckOverwrite descriptors for string canonicalization.
import * as z from "zod";

// Reusable check descriptors across classic and mini schemas
const nonEmptyTrimmed = [z.trim(), z.minLength(1, "Cannot be blank")] as const;

const TitleSchema = z.string().check(...nonEmptyTrimmed, z.maxLength(120));
const UploadSchema = z.file().check(
  z.minSize(1024),
  z.maxSize(5 * 1024 * 1024),
  z.mime(["image/png", "image/webp"]),
);

functionz.string Check & Transform Methods#

In Classic zod, ZodString exposes ergonomic method chains that wrap the underlying $ZodCheck factories and expose derived metadata getters (.minLength, .maxLength, .format).

ZodString Method Reference#

Method Underlying $ZodCheck Mutates Value? Description
.min(minLength, params?) $ZodCheckMinLength No Requires str.length >= minLength. Updates schema.minLength.
.max(maxLength, params?) $ZodCheckMaxLength No Requires str.length <= maxLength. Updates schema.maxLength.
.length(len, params?) $ZodCheckLengthEquals No Requires str.length === len. Sets both minLength and maxLength.
.nonempty(params?) $ZodCheckMinLength(1) No Equivalent to .min(1, params).
.regex(regex, params?) $ZodCheckRegex No Requires regex.test(str) to pass.
.includes(value, params?) $ZodCheckIncludes No Requires str.includes(value).
.startsWith(value, params?) $ZodCheckStartsWith No Requires str.startsWith(value).
.endsWith(value, params?) $ZodCheckEndsWith No Requires str.endsWith(value).
.lowercase(params?) $ZodCheckLowerCase No Validates str === str.toLowerCase() (does not mutate).
.uppercase(params?) $ZodCheckUpperCase No Validates str === str.toUpperCase() (does not mutate).
.trim() $ZodCheckOverwrite Yes Strips leading and trailing whitespace via str.trim() at the exact position in the check chain.
.toLowerCase() $ZodCheckOverwrite Yes Converts string to lowercase in place via str.toLowerCase().
.toUpperCase() $ZodCheckOverwrite Yes Converts string to uppercase in place via str.toUpperCase().
.normalize(form?) $ZodCheckOverwrite Yes Unicode-normalizes string via str.normalize(form) ("NFC" default, "NFD", "NFKC", "NFKD").
.slugify() $ZodCheckOverwrite Yes Converts string into a lowercase URL-safe ASCII slug.

[!TIP] Because .trim(), .toLowerCase(), and .normalize() are $ZodCheck descriptors stored in def.checks, execution order follows method chaining order: z.string().trim().min(3) trims whitespace first and then checks length >= 3, whereas z.string().min(3).trim() checks length before trimming.


functionz.number Bounds & Check Methods#

ZodNumber and ZodBigInt provide fluent bound, sign, and divisibility methods backed by $ZodCheckGreaterThan, $ZodCheckLessThan, $ZodCheckMultipleOf, and $ZodCheckNumberFormat.

ZodNumber Method & Property Reference#

Method / Property Equivalent Check / Type Description
.gt(value, params?) z.gt(value, params) Strictly greater than (x > value; exclusiveMinimum in JSON Schema).
.gte(value, params?) / .min(value, params?) z.gte(value, params) Greater than or equal to (x >= value; minimum in JSON Schema).
.lt(value, params?) z.lt(value, params) Strictly less than (x < value; exclusiveMaximum in JSON Schema).
.lte(value, params?) / .max(value, params?) z.lte(value, params) Less than or equal to (x <= value; maximum in JSON Schema).
.positive(params?) z.gt(0, params) Strictly positive (x > 0).
.nonnegative(params?) z.gte(0, params) Zero or positive (x >= 0).
.negative(params?) z.lt(0, params) Strictly negative (x < 0).
.nonpositive(params?) z.lte(0, params) Zero or negative (x <= 0).
.multipleOf(value, params?) / .step(value, params?) z.multipleOf(value, params) Float-safe remainder check (floatSafeRemainder(x, value) === 0).
.int(params?) / .safe(params?) $ZodCheckNumberFormat("safeint") Restricts number to safe integers (Number.isSafeInteger(x)). Prefer top-level z.int() for standalone integer schemas.
schema.minValue / schema.maxValue number | null Aggregated minimum/maximum bound across all attached checks.
schema.format string | null Aggregated numeric format ("safeint", "int32", "uint32", "float32", "float64", or null).

methodZodType.overwrite#

.overwrite(fn) (and standalone z.overwrite(fn)) executes an in-place transformation inside the def.checks array without wrapping the schema in a ZodPipe or altering its static TypeScript input/output type.

Signature & Comparison with .transform()#

Feature schema.overwrite(fn) / z.overwrite(fn) schema.transform(fn)
Static Type (z.input<S> vs z.output<S>) Preserved (Input === Output === T) Changes Output to NewOut (ZodPipe<S, ZodTransform<NewOut, O>>)
Underlying Representation $ZodCheckOverwrite<T> appended to def.checks Wraps schema in a ZodPipe with $ZodTransform
JSON Schema Compatibility Representable (transparent to z.toJSONSchema()) Unrepresentable in "output" mode unless overridden
z.encode() Compatibility Runs in both forward (decode) and backward (encode) directions Throws $ZodEncodeError on z.encode() (unidirectional)
import * as z from "zod";

// Canonicalize a tag array in place while keeping JSON Schema & encode() support
const TagsSchema = z
  .array(z.string().trim().toLowerCase())
  .overwrite((tags) => Array.from(new Set(tags)).sort());

TagsSchema.parse(["  Zod ", "typescript", "zod"]); // => ["typescript", "zod"]

classZodTransform & ZodPipe#

Unidirectional data transformations and multi-schema validation chains are built on ZodTransform<O, I> and ZodPipe<A, B>.

Constructors & Methods#

API Signature Return Type Description
schema.transform(fn) transform<NewOut>(fn: (arg: O, ctx: RefinementCtx<O>) => MaybeAsync<NewOut>) ZodPipe<this, ZodTransform<Awaited<NewOut>, O>> Validates with this, then maps the output through fn. ctx.addIssue(...) or returning z.NEVER aborts the pipeline.
z.transform(fn) z.transform<I = unknown, O = I>(fn: (input: I, ctx: ParsePayload) => O) ZodTransform<Awaited<O>, I> Creates a standalone transformation node without an input validator.
schema.pipe(target) pipe<T extends $ZodType<any, O>>(target: T) ZodPipe<this, T> Chains this (in schema) into target (out schema), feeding output<this> into input<T>.
z.pipe(inSchema, outSchema) z.pipe<A, B>(in_: A, out: B) ZodPipe<A, B> Functional constructor for ZodPipe (used in both zod and zod/mini).
pipeSchema.in / pipeSchema.out Readonly properties on ZodPipe<A, B> A / B Direct references to the upstream (in) and downstream (out) schemas.
import * as z from "zod";

const CsvIntsSchema = z
  .string()
  .transform((s) => s.split(",").map((part) => part.trim()))
  .pipe(z.array(z.coerce.number().int()));

type In = z.input<typeof CsvIntsSchema>;   // string
type Out = z.output<typeof CsvIntsSchema>; // number[]

classZodCodec & z.codec#

Introduced in Zod v4, ZodCodec<A, B> extends ZodPipe<A, B> with a bidirectional pair of transformation functions (decode and encode), enabling type-safe serialization and deserialization through a single schema definition.

03_checks_refinements_codecs_2
Bidirectional Execution Flow of ZodCodec in Forward (Decode) vs Backward (Encode) Modes
Source Code
from drawlib import canvas
from drawlib.diagrams.sequence import Participant, ParticipantGroup, PhosphorIcon, SequenceDiagram
from drawlib.styles import Styles

canvas.clear()
canvas.setup(width=166, height=106, dpi=150)

d = SequenceDiagram(
    node_style=Styles.Primary,
    node_text_style=Styles.DarkBold.patch(text_size=10.5),
    edge_style=Styles.DarkBold,
    edge_text_style=Styles.Dark.patch(text_size=10.0),
    node_card_style=Styles.Neutral,
    title="ZodCodec<InSchema, OutSchema> — Forward Decode vs Backward Encode",
    autonumber=True,
    col_width=36.0,
    step_y=6.5,
)

caller = d.add(Participant("Caller App", width=22, height=13, icon=PhosphorIcon.TERMINAL_WINDOW, icon_size=6.0, card_style=Styles.Neutral))

codec_grp = d.add(ParticipantGroup(title="ZodCodec<InSchema, OutSchema>", padding=4.0, style=Styles.Neutral))
in_schema = codec_grp.add(Participant("inSchema\n(A: Wire Type)", width=24, height=13, icon=PhosphorIcon.FILE_TEXT, icon_size=6.0, card_style=Styles.Neutral))
xform = codec_grp.add(Participant("Codec Bridge\n{decode, encode}", width=26, height=13, icon=PhosphorIcon.ARROWS_LEFT_RIGHT, icon_size=6.0, style=Styles.WhiteBold, card_style=Styles.PrimaryFlat, text_style=Styles.WhiteBold.patch(text_size=10.5)))
out_schema = codec_grp.add(Participant("outSchema\n(B: Domain Type)", width=24, height=13, icon=PhosphorIcon.CUBE, icon_size=6.0, card_style=Styles.PrimaryNeutral))

with d.alt("Forward (decode / parse)") as blk:
    blk.style = Styles.Neutral.patch(line_style="dashed")
    caller.request(in_schema, "Validate wireInput (A._zod.run)")
    in_schema.request(xform, "decode(output<A>, payload)")
    xform.request(out_schema, "Validate input<B> & pipe checks")
    out_schema.reply(caller, "Return output<B> (Domain Value)", style=Styles.Success, text_style=Styles.Success.patch(text_size=10.0))

    d.else_("Backward (encode / safeEncode)")
    caller.request(out_schema, "Validate domainValue (B._zod.run)")
    out_schema.request(xform, "encode(input<B>, payload)")
    xform.request(in_schema, "Validate output<A> (A._zod.run)")
    in_schema.reply(caller, "Return input<A> (Wire Value)", style=Styles.Accent, text_style=Styles.Accent.patch(text_size=10.0))

d.draw(xy=(83.0, 51.0), align="center")
canvas.save()

z.codec & z.invertCodec Signatures#

Function / Method Signature Return Type Description
z.codec(in_, out, { decode, encode }) codec<A, B>(in_: A, out: B, params: { decode: (val: output<A>, payload: ParsePayload<output<A>>) => MaybeAsync<input<B>>; encode: (val: input<B>, payload: ParsePayload<input<B>>) => MaybeAsync<output<A>> }) ZodCodec<A, B> Creates a bidirectional codec between wire schema A and domain schema B.
z.invertCodec(codec) invertCodec<A, B>(codec: ZodCodec<A, B>) ZodCodec<B, A> Returns a new ZodCodec<B, A> with in and out swapped and decode/encode reversed.
z.stringbool(params?) stringbool(params?: string | $ZodStringBoolParams) ZodCodec<ZodString, ZodBoolean> Built-in codec converting environment-variable strings ("true", "1", "yes", "on" vs "false", "0", "no", "off") to boolean and back.
import * as z from "zod";

const IsoDateCodec = z.codec(z.iso.datetime(), z.date(), {
  decode: (isoString) => new Date(isoString),
  encode: (dateObj) => dateObj.toISOString(),
});

// Forward (decode): string -> Date
const parsedDate = z.decode(IsoDateCodec, "2026-03-30T12:00:00.000Z");

// Backward (encode): Date -> string
const wireString = z.encode(IsoDateCodec, new Date("2026-03-30T12:00:00.000Z"));

// Invert codec: Date -> string (forward), string -> Date (backward)
const DateToIsoCodec = z.invertCodec(IsoDateCodec);

functionz.preprocess#

z.preprocess(fn, schema) constructs a ZodPreprocess<U, B> (a specialized ZodPipe<ZodTransform<unknown, B>, U>) that runs fn(input, ctx) before passing the result to schema.

Signature & Migration Notes#

Function Signature Inferred Types (z.input / z.output) Description
z.preprocess(fn, schema) preprocess<A, U extends SomeType, B = unknown>(fn: (arg: B, ctx: RefinementCtx) => A, schema: U): ZodPreprocess<U, B> input: B (unknown by default), output: z.output<U> Transforms raw unvalidated input via fn before running schema. In z.toJSONSchema(), ZodPreprocess transparently delegates to schema.
import * as z from "zod";

// Prefer z.pipe() or z.codec() when the input type is known; use z.preprocess() for unknown raw inputs
const TrimmedNumber = z.preprocess(
  (val) => (typeof val === "string" ? val.trim() : val),
  z.coerce.number().int(),
);