Skip to Content

Last Updated: 3/12/2026


Records

Record schemas are used to validate types such as Record<string, string>. They represent objects with dynamic keys but consistent value types.

Basic Records

import * as z from "zod"; const IdCache = z.record(z.string(), z.string()); type IdCache = z.infer<typeof IdCache>; // Record<string, string> IdCache.parse({ carlotta: "77d2586b-9e8e-4ecf-8b21-ea7e0530eadd", jimmie: "77d2586b-9e8e-4ecf-8b21-ea7e0530eadd", }); // ✅

Key Schemas

The key schema can be any Zod schema that is assignable to string | number | symbol.

const Keys = z.union([z.string(), z.number(), z.symbol()]); const AnyObject = z.record(Keys, z.unknown()); // Record<string | number | symbol, unknown>

Enum Keys

To create an object schema containing keys defined by an enum:

const Keys = z.enum(["id", "name", "email"]); const Person = z.record(Keys, z.string()); // { id: string; name: string; email: string }

Numeric Keys

New — As of v4.2, Zod properly supports numeric keys inside records. A number schema, when used as a record key, will validate that the key is a valid “numeric string”. Additional numerical constraints (min, max, step, etc.) will also be validated.

const numberKeys = z.record(z.number(), z.string()); numberKeys.parse({ 1: "one", // ✅ 2: "two", // ✅ "1.5": "one point five", // ✅ "-3": "negative three", // ✅ abc: "one" // ❌ throws }); // Further validation is also supported const intKeys = z.record(z.int().min(0).max(10), z.string()); intKeys.parse({ 0: "zero", // ✅ 1: "one", // ✅ 2: "two", // ✅ 12: "twelve", // ❌ throws (exceeds max) abc: "one" // ❌ throws (not numeric) });

Partial Records

Zod 4 — In Zod 4, if you pass a z.enum as the first argument to z.record(), Zod will exhaustively check that all enum values exist in the input as keys. This behavior agrees with TypeScript:

type MyRecord = Record<"a" | "b", string>; const myRecord: MyRecord = { a: "foo", b: "bar" }; // ✅ const myRecord: MyRecord = { a: "foo" }; // ❌ missing required key `b`

To create a partial record type, use z.partialRecord(). This skips the special exhaustiveness checks:

const Keys = z.enum(["id", "name", "email"]); const Person = z.partialRecord(Keys, z.string()); // { id?: string; name?: string; email?: string } Person.parse({ id: "123" }); // ✅ (name and email are optional)

Loose Records

By default, z.record() errors on keys that don’t match the key schema. Use z.looseRecord() to pass through non-matching keys unchanged. This is particularly useful when combined with intersections to model multiple pattern properties:

const schema = z .object({ name: z.string() }) .and(z.looseRecord(z.string().regex(/_phone$/), z.string())); type schema = z.infer<typeof schema>; // => { name: string } & Record<string, string> schema.parse({ name: "John", home_phone: "+12345678900", // validated as phone number work_phone: "+12345678900", // validated as phone number other_field: "passes through" // not validated, passes through }); // ✅

Records vs Objects

Records

  • Dynamic keys (keys determined at runtime)
  • Uniform value types (all values have the same schema)
  • Unknown number of properties
const userScores = z.record(z.string(), z.number()); // { [username: string]: number }

Objects

  • Fixed keys (keys known at compile time)
  • Different value types per property
  • Known properties
const user = z.object({ name: z.string(), age: z.number(), active: z.boolean() });

Common Use Cases

Configuration Maps

const ConfigMap = z.record(z.string(), z.string()); // Environment variables, feature flags, etc.

ID Mappings

const UserIdMap = z.record(z.string(), z.object({ id: z.string(), name: z.string(), email: z.string() })); // Map user IDs to user objects

Translations/i18n

const Translations = z.record( z.enum(["en", "es", "fr"]), z.record(z.string(), z.string()) ); // { en: { greeting: "Hello" }, es: { greeting: "Hola" }, ... }

Dynamic Form Data

const FormData = z.record(z.string(), z.union([ z.string(), z.number(), z.boolean() ]));

Best Practices

  1. Use records when keys are dynamic and unknown at compile time
  2. Use objects when the structure is fixed and known
  3. Use z.partialRecord() when not all enum keys are required
  4. Use z.looseRecord() for pattern-based validation with pass-through
  5. Combine with intersections to mix fixed and dynamic properties