Capítulo 21 de 61

Chapter 21: Records

Core Idea

z.record(keySchema, valueSchema) validates dictionary-shaped objects (Record<K, V>), with three variants controlling how strictly keys are checked: exhaustive (default with enum keys), partial, and loose (pass-through).

Key Concepts

  • z.record(keySchema, valueSchema): the key schema must be assignable to string | number | symbol; numeric key schemas validate that keys are valid numeric strings (and can carry further numeric constraints like .min()/.step()).
  • Exhaustiveness with enum/literal keys: if the key schema is z.enum() or z.literal(), z.record() requires every possible key to be present, mirroring TypeScript's Record<"a"|"b", string> behavior.
  • z.partialRecord(keySchema, valueSchema): skips that exhaustiveness check, so an enum/literal-keyed record can have a subset of its keys.
  • z.looseRecord(keySchema, valueSchema): unlike the default (which errors on keys not matching the key schema), passes through non-matching keys unchanged — useful combined with .and() to model "one specific field plus a pattern of extra fields."

Code Examples

const IdCache = z.record(z.string(), z.string()); // Record<string, string>

// numeric keys validate as numeric strings, with numeric constraints
const intKeys = z.record(z.int().step(1).min(0).max(10), z.string());
intKeys.parse({ 0: "zero", 12: "twelve" }); // "12" fails — out of range

// enum keys require every key present (exhaustive)
const Keys = z.enum(["id", "name", "email"]);
const Person = z.record(Keys, z.string()); // all 3 keys required

// partial: skip the exhaustiveness check
const PartialPerson = z.partialRecord(Keys.or(z.never()), z.string());

// loose: pass through keys that don't match the key schema
const schema = z
  .object({ name: z.string() })
  .and(z.looseRecord(z.string().regex(/_phone$/), z.e164()));
  • What it demonstrates: strict/exhaustive, partial, and loose record variants for different key-coverage requirements.

Anti-patterns

  • Using plain z.record() with enum keys when you only want some of them present: the default is exhaustive — every enum value must appear as a key — use z.partialRecord() instead.

Key Takeaways

  1. z.record() with an enum/literal key schema is exhaustive by default — it mirrors TypeScript's Record<K, V> semantics, not a "maybe has these keys" dictionary.
  2. Reach for z.partialRecord() the moment you need a subset of enum-keyed fields.
  3. z.looseRecord() combined with .and() (intersection) is the pattern for "known field + arbitrary pattern-matched extra fields."

Connects To

  • Objects — Definition, Strictness & Shape: .catchall() solves a similar "unknown keys" problem for a fixed-shape object rather than a pure dictionary.
  • Maps & Sets: for a Map/Set runtime type instead of a plain object.