Capítulo 37 de 61

Chapter 37: How Encoding Interacts With Other Schema Features

Core Idea

.encode() runs the schema "in reverse," and different schema features behave differently under reversal: refinements and mutating checks still apply both ways, but defaults, prefaults, and unidirectional transforms only make sense going forward and either get skipped or throw during encode.

Key Concepts

  • Refinements run in both directions: .refine(), .min(), .max(), etc. are checked whether you .decode() or .encode(). Internally, .encode() runs two passes — first confirming the input matches the expected type, then running refinement logic — so custom refinements don't see unexpected shapes.
  • Mutating checks (.trim(), .toLowerCase()) apply during encode too: schema.encode(" hello ") trims just like .decode() does.
  • Defaults/prefaults apply only going forward: since a default makes the input optional but not the output, undefined is not a valid argument to .encode() — attempting it throws.
  • .catch() applies only going forward: encoding an invalid value does not fall back to the catch value; it throws a ZodError instead.
  • z.stringbool() encodes using the first truthy/falsy value: if you customized truthy/falsy arrays, .encode(true)/.encode(false) use the first element of the respective array.
  • .transform() is strictly unidirectional: any transform anywhere in the schema makes .encode() throw a runtime Error (not a ZodError) — transforms have no defined reverse operation.

Code Examples

const schema = stringToDate.refine((date) => date.getFullYear() >= 2000, "Must be this millennium");
schema.encode(new Date("1999-01-01")); // throws ZodError — refinement fails both ways

const stringWithDefault = z.string().default("hello");
stringWithDefault.decode(undefined); // => "hello"
stringWithDefault.encode(undefined); // throws — undefined isn't a valid output-side input

const stringbool = z.stringbool({ truthy: ["yes", "y"], falsy: ["no", "n"] });
stringbool.encode(true);  // => "yes" (first truthy value)
stringbool.encode(false); // => "no"  (first falsy value)

const transformed = z.string().transform(val => val.length);
transformed.encode(1234); // throws a plain Error, not ZodError — transforms can't reverse
  • What it demonstrates: which schema features survive the reverse (encode) direction unchanged, and which ones either no-op-fail or throw.

Anti-patterns

  • Building a schema with .transform() and expecting .encode() to work on it: it can't — any unidirectional transform anywhere in the schema makes encoding throw. Use a codec instead if you need both directions.
  • Assuming .catch()'s fallback applies during .encode(): it doesn't — encode-side failures throw normally.

Key Takeaways

  1. If a schema needs to work both directions (decode and encode), avoid .transform() anywhere in it — use z.codec() for the genuinely two-way piece instead.
  2. Defaults, prefaults, and catch are all forward-only conveniences; they silently stop applying (or actively throw) when encoding.
  3. Refinements and mutating checks (trim, case conversion) are the schema features that safely apply in both directions.

Connects To

  • Codecs — Encode/Decode Mechanics: the core decode/encode API these interactions layer onto.
  • Defaults, Prefaults & Catch: the forward-only fallback mechanisms discussed here in more depth.