Capítulo 33 de 61

Chapter 33: Custom & Apply

Core Idea

z.custom<T>() lets you validate any TypeScript type with a hand-written predicate when no built-in schema fits; .apply() lets you inject reusable, external logic into a schema's method chain.

Key Concepts

  • z.custom<T>(validatorFn): validates T-typed values using an arbitrary predicate function. Prefer z.instanceof() for class instances and z.templateLiteral() for template literal types instead — z.custom() is the fallback for everything else (e.g. third-party library types like Decimal).
  • No validator = no validation: z.custom<T>() called with no arguments accepts any value and performs zero runtime checks — a real footgun if used carelessly.
  • Custom error message: pass a second argument (string or the same params object .refine() accepts) to customize the failure message.
  • .apply(fn, ...args): calls fn(schema, ...args) inline in a method chain, so reusable schema-building logic (e.g. "apply our standard number range checks") can be shared across schemas without breaking the fluent chain.

Code Examples

import { Decimal } from "decimal.js";

const decimalSchema = z.custom<Decimal>((val) => Decimal.isDecimal(val));
decimalSchema.parse(new Decimal("1.5")); // passes
decimalSchema.parse("1.5");              // throws

// apply: inject reusable logic into a chain
function setCommonNumberChecks<T extends z.ZodNumber>(schema: T) {
  return schema.min(0).max(100);
}

const schema = z.number().apply(setCommonNumberChecks).nullable();
schema.parse(0);    // ok
schema.parse(101);  // throws
schema.parse(null); // ok

// apply passes extra arguments through to the function
function withDefault<T extends z.ZodType>(schema: T, value: z.output<T>) {
  return schema.nullish().transform((val) => val ?? value);
}
const withFallback = z.string().apply(withDefault, "anonymous");
withFallback.parse(undefined); // => "anonymous"
  • What it demonstrates: validating a third-party type with a predicate, and reusing a chain-of-checks function across multiple schemas via .apply().

Anti-patterns

  • Calling z.custom<T>() with no validator "just to get the type": it performs zero validation — any value passes — which defeats the point of a runtime schema and can silently let invalid data through.
  • Reaching for z.custom() for class instances or template literals: z.instanceof() and z.templateLiteral() are the purpose-built, more precise tools for those cases.

Key Takeaways

  1. z.custom<T>() is the last resort for types nothing else covers — always supply a real validator function.
  2. .apply() keeps shared validation logic (like standard range checks) reusable and chainable, instead of copy-pasted across schemas.
  3. Extra arguments to .apply(fn, ...args) pass straight through to fn, enabling parameterized reusable schema builders.

Connects To

  • Instanceof: the more precise tool for class-instance validation.
  • Template Literals: the more precise tool for TS template literal types.