Basic Usage
Create a schema for objects with dynamic keys, where all values share the same type.
import { z } from 'zod';
const StringRecord = z.record(z.string(), z.number());
StringRecord.parse({
alice: 100,
bob: 200,
}); // ✓ Valid
StringRecord.parse({
alice: 100,
bob: 'invalid',
}); // ✗ Invalid - value must be number
type StringRecord = z.infer<typeof StringRecord>;
// Record<string, number>
// Same as: { [key: string]: number }
Signature
function record<Key extends ZodRecordKey, Value extends SomeType>(
keyType: Key,
valueType: Value,
params?: string | ZodRecordParams
): ZodRecord<Key, Value>
keyType
ZodString | ZodNumber | ZodEnum
required
Schema to validate keys. Can be z.string(), z.number(), or z.enum().
Schema to validate all values.
Optional error message (string) or configuration object.
Properties
Access the key schema.const schema = z.record(z.string(), z.number());
schema.keyType; // ZodString
Access the value schema.const schema = z.record(z.string(), z.number());
schema.valueType; // ZodNumber
Key Types
String Keys
Most common use case:
const StringKeys = z.record(z.string(), z.boolean());
type StringKeys = z.infer<typeof StringKeys>;
// Record<string, boolean>
StringKeys.parse({
feature1: true,
feature2: false,
}); // ✓ Valid
Number Keys
For numeric indices:
const NumberKeys = z.record(z.number(), z.string());
type NumberKeys = z.infer<typeof NumberKeys>;
// Record<number, string>
NumberKeys.parse({
0: 'first',
1: 'second',
2: 'third',
}); // ✓ Valid
In JavaScript, numeric keys are automatically converted to strings. The schema accepts both { 0: 'value' } and { '0': 'value' }.
Enum Keys
Restrict keys to specific values:
const Status = z.enum(['idle', 'loading', 'success', 'error']);
const StatusMessages = z.record(Status, z.string());
type StatusMessages = z.infer<typeof StatusMessages>;
// Record<"idle" | "loading" | "success" | "error", string>
StatusMessages.parse({
idle: 'Ready to start',
loading: 'Loading...',
success: 'Complete!',
error: 'Failed',
}); // ✓ Valid
StatusMessages.parse({
idle: 'Ready',
unknown: 'Invalid', // ✗ Invalid - unknown key
});
Complex Value Types
Object Values
const UserRecords = z.record(
z.string(),
z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
})
);
type UserRecords = z.infer<typeof UserRecords>;
// Record<string, { name: string; age: number; email: string }>
UserRecords.parse({
user1: { name: 'Alice', age: 30, email: '[email protected]' },
user2: { name: 'Bob', age: 25, email: '[email protected]' },
}); // ✓ Valid
Array Values
const TagLists = z.record(z.string(), z.array(z.string()));
type TagLists = z.infer<typeof TagLists>;
// Record<string, string[]>
TagLists.parse({
article1: ['javascript', 'typescript', 'zod'],
article2: ['react', 'hooks'],
}); // ✓ Valid
Union Values
const MixedConfig = z.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()])
);
type MixedConfig = z.infer<typeof MixedConfig>;
// Record<string, string | number | boolean>
MixedConfig.parse({
host: 'localhost',
port: 3000,
debug: true,
}); // ✓ Valid
Partial Records
Create records where not all keys are required:
const Features = z.enum(['dark-mode', 'analytics', 'notifications']);
// Regular record - all keys required
const AllFeatures = z.record(Features, z.boolean());
AllFeatures.parse({
'dark-mode': true,
'analytics': false,
'notifications': true,
}); // ✓ Valid - all keys present
// Partial record - keys optional
const SomeFeatures = z.partialRecord(Features, z.boolean());
SomeFeatures.parse({
'dark-mode': true,
}); // ✓ Valid - partial keys allowed
type SomeFeatures = z.infer<typeof SomeFeatures>;
// Partial<Record<"dark-mode" | "analytics" | "notifications", boolean>>
Signature:
function partialRecord<Key extends ZodRecordKey, Value extends SomeType>(
keyType: Key,
valueType: Value,
params?: string | ZodRecordParams
): ZodRecord<Key & Partial, Value>
Loose Records
Create records that allow unrecognized keys:
const RequiredFields = z.enum(['id', 'name']);
const LooseRecord = z.looseRecord(RequiredFields, z.string());
LooseRecord.parse({
id: '123',
name: 'Alice',
email: '[email protected]', // Extra keys allowed
phone: '555-0100',
}); // ✓ Valid
type LooseRecord = z.infer<typeof LooseRecord>;
// Record<"id" | "name", string> & { [key: string]: unknown }
Signature:
function looseRecord<Key extends ZodRecordKey, Value extends SomeType>(
keyType: Key,
valueType: Value,
params?: string | ZodRecordParams
): ZodRecord<Key, Value>
Type Inference
const schema = z.record(
z.string(),
z.object({ count: z.number() })
);
type Output = z.infer<typeof schema>;
// Record<string, { count: number }>
type Input = z.input<typeof schema>;
// Same as output for records without transformations
Common Patterns
Configuration Objects
const Config = z.record(
z.string(),
z.union([z.string(), z.number(), z.boolean()])
);
type Config = z.infer<typeof Config>;
// Record<string, string | number | boolean>
const config: Config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
debug: true,
};
Translation Dictionaries
const Locales = z.enum(['en', 'es', 'fr']);
const Translations = z.record(
Locales,
z.record(z.string(), z.string())
);
type Translations = z.infer<typeof Translations>;
// Record<"en" | "es" | "fr", Record<string, string>>
const translations: Translations = {
en: { greeting: 'Hello', farewell: 'Goodbye' },
es: { greeting: 'Hola', farewell: 'Adiós' },
fr: { greeting: 'Bonjour', farewell: 'Au revoir' },
};
Counters and Metrics
const Metrics = z.record(z.string(), z.number().nonnegative());
type Metrics = z.infer<typeof Metrics>;
// Record<string, number>
const metrics: Metrics = {
pageViews: 1000,
uniqueVisitors: 250,
bounceRate: 0.35,
};
Entity Stores
const User = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
const UserStore = z.record(z.string(), User);
type UserStore = z.infer<typeof UserStore>;
// Record<string, { id: string; name: string; email: string }>
const store: UserStore = {
'user-1': { id: 'user-1', name: 'Alice', email: '[email protected]' },
'user-2': { id: 'user-2', name: 'Bob', email: '[email protected]' },
};
Refinements
Add custom validation:
const NonEmptyRecord = z.record(z.string(), z.number())
.refine(
(record) => Object.keys(record).length > 0,
'Record cannot be empty'
);
NonEmptyRecord.parse({ a: 1 }); // ✓ Valid
NonEmptyRecord.parse({}); // ✗ Invalid - empty record
const AllPositive = z.record(z.string(), z.number())
.refine(
(record) => Object.values(record).every(v => v > 0),
'All values must be positive'
);
AllPositive.parse({ a: 1, b: 2 }); // ✓ Valid
AllPositive.parse({ a: 1, b: -2 }); // ✗ Invalid - negative value
Record vs Object
| Feature | Record | Object |
|---|
| Keys | Dynamic | Fixed |
| Key Type | String/Number/Enum | String literals |
| Value Type | All same | Can vary |
| Type Inference | Record<K, V> | { key: Type } |
| Use Case | Unknown keys | Known structure |
// Record - dynamic keys, same value type
const record = z.record(z.string(), z.number());
type Record = z.infer<typeof record>;
// Record<string, number>
// Object - fixed keys, varying types
const object = z.object({
name: z.string(),
age: z.number(),
});
type Object = z.infer<typeof object>;
// { name: string; age: number }
Transform record values:
const Normalized = z.record(z.string(), z.string())
.transform((record) => {
const normalized: Record<string, string> = {};
for (const [key, value] of Object.entries(record)) {
normalized[key.toLowerCase()] = value.trim();
}
return normalized;
});
const result = Normalized.parse({
'FIRST_NAME': ' Alice ',
'LAST_NAME': ' Smith ',
});
// result: { first_name: 'Alice', last_name: 'Smith' }