Capítulo 36 de 61

Chapter 36: Codecs — Encode/Decode Mechanics

Core Idea

Every Zod schema technically supports forward (.parse()/.decode()) and backward (.encode()) processing, but the distinction only matters when input and output types diverge — which is exactly what z.codec() is built for, and it composes like any other schema (nestable in objects/arrays/pipes).

Key Concepts

  • Forward vs. backward: .parse()/.decode() go input → output; .encode() goes output → input. For most schemas input and output types are identical, so the distinction is invisible; for codecs they genuinely differ.
  • z.invertCodec(codec): swaps input/output schemas and the decode/encode functions — but only for the codec passed in, not recursively for codecs nested deeper inside it.
  • Codecs nest freely: they're ordinary schemas — usable inside z.object(), arrays, pipes, with no special restrictions. objectSchema.decode({...}) decodes every nested codec field in one call.
  • Type-safe .decode()/.encode() vs. unknown-typed .parse(): .parse() accepts unknown (runtime-checked); .decode()/.encode() have strongly-typed parameters matching the codec's input/output type respectively, so passing the wrong type is a compile error, not just a runtime one.
  • Async & safe variants: codecs support async decode/encode functions; .decodeAsync()/.encodeAsync() return promises, and .safeDecode()/.safeDecodeAsync() (plus the encode equivalents) return a result object instead of throwing.

Code Examples

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

// nested inside an object — decodes the whole payload in one call
const payloadSchema = z.object({ startDate: stringToDate });
payloadSchema.decode({ startDate: "2024-01-15T10:30:00.000Z" });
// => { startDate: Date }

// type-safe inputs: parse() accepts unknown, decode()/encode() don't
stringToDate.parse(12345);  // no compile error (fails at runtime)
stringToDate.decode(12345); // compile error — expects a string

// async + safe variants
stringToDate.decodeAsync("2024-01-15T10:30:00.000Z");   // Promise<Date>
stringToDate.safeDecode("2024-01-15T10:30:00.000Z");
// => { success: true, data: Date } | { success: false, error: ZodError }
  • What it demonstrates: codecs composing inside objects, and the stricter compile-time typing of .decode()/.encode() versus .parse().

Anti-patterns

  • Assuming z.invertCodec() inverts codecs nested inside the one you pass it: it doesn't — invert nested codecs individually where you build the containing schema.

Key Takeaways

  1. .decode()/.encode() exist specifically because .parse()'s unknown input type can't catch obviously-wrong-typed calls at compile time — use them when the input is already typed in your application code.
  2. Codecs compose transparently with the rest of the schema API — no special handling needed to nest them in objects or arrays.
  3. Reach for the safe/async variants (.safeDecode(), .decodeAsync(), etc.) the same way you would with .safeParse()/.parseAsync().

Connects To

  • Codecs — Basics: the core z.codec() API these mechanics build on.
  • Pipes: z.input()/z.output() for reaching codecs nested deep inside a schema tree.