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>.
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$ZodCheckdescriptors stored indef.checks, execution order follows method chaining order:z.string().trim().min(3)trims whitespace first and then checks length>= 3, whereasz.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.
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(),
);