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 objectsTranslations/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
- Use records when keys are dynamic and unknown at compile time
- Use objects when the structure is fixed and known
- Use
z.partialRecord()when not all enum keys are required - Use
z.looseRecord()for pattern-based validation with pass-through - Combine with intersections to mix fixed and dynamic properties