Skip to main content

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().
valueType
ZodType
required
Schema to validate all values.
params
string | ZodRecordParams
Optional error message (string) or configuration object.

Properties

keyType
ZodType
Access the key schema.
const schema = z.record(z.string(), z.number());
schema.keyType; // ZodString
valueType
ZodType
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

FeatureRecordObject
KeysDynamicFixed
Key TypeString/Number/EnumString literals
Value TypeAll sameCan vary
Type InferenceRecord<K, V>{ key: Type }
Use CaseUnknown keysKnown 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 }

Transformations

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' }

Build docs developers (and LLMs) love