Skip to content

Zod Object with Dynamic Keys

When validating objects with dynamic keys in Zod, many developers familiar with TypeScript's Record<string, string> try to create a computed property schema. However, this common approach using z.object({ [z.string()]: z.string() }) doesn't work as expected. This article explains the proper Zod solution for validating objects with arbitrary keys.

Key Concept

Zod objects are designed for known keys with specific validation rules. For dynamic keys where the key names are unpredictable, you need a different approach.

The Ideal Solution: z.record()

Use Zod's record() method to validate objects with dynamic string keys and consistent value types. This replicates TypeScript's Record<K, V> behavior.

js
import { z } from "zod";

// Validate object with string keys and string values
const stringMapSchema = z.record(z.string(), z.string());

// Valid usage
stringMapSchema.parse({ 
  key1: "value1",
  dynamicKey: "anotherValue" 
}); // Success

// Invalid usage
stringMapSchema.parse({
  validKey: "ok",
  numberKey: 123  // Fails - number instead of string
});

Parameter Breakdown

  • First argument: Key validator (typically z.string() or z.number())
  • Second argument: Value validator (any Zod type)

Common Use Cases

js
// API responses with dynamic keys
const apiResponseSchema = z.record(z.string(), z.object({
  id: z.number(),
  name: z.string()
}));

// Environment variables
const envSchema = z.record(z.string(), z.string().or(z.number()));

Limitations

z.record() validates when all keys and all values match the specified schemas. For partially dynamic objects where some keys are known and others are dynamic, use .catchall().

Alternative Approach: .catchall()

For mixed objects containing both known and dynamic keys, use catchall():

js
const userSchema = z.object({
  name: z.string(),
  age: z.number(),
}).catchall(z.string()); // All unknown keys must be strings

// Valid
userSchema.parse({
  name: "Alice",
  age: 30,
  metadata1: "extra",
  meta2: "additional"
});

// Invalid: Unknown key has wrong type
userSchema.parse({
  name: "Bob",
  age: 25,
  tags: [1, 2, 3]  // Array instead of string
});

Key Differences: .record() vs .catchall()

Featurez.record().catchall()
Key RequirementsAll keys dynamically typedMixed known + dynamic keys
Empty ObjectsValidRequires known keys
Use CaseFully dynamic mapsExtending fixed schemas

TypeScript Equivalence

Zod's solutions provide automatic TypeScript type inference:

ts
// z.record() infers as Record<string, string>
type StringMap = z.infer<typeof stringMapSchema>;
// { [k: string]: string }

// .catchall() infers exact types
type User = z.infer<typeof userSchema>;
// { name: string; age: number; [k: string]: string }

Dynamic Key Validation

For complex key validation (like regex patterns), combine with .refine():

js
const schema = z.record(z.string().refine(key => /^id-\d+$/.test(key)), z.number());

schema.parse({ "id-123": 1 }); // Valid
schema.parse({ "name": 2 });   // Fails key validation

By using z.record() for fully dynamic objects and .catchall() for mixed structures, you can accurately handle dynamic keys while leveraging Zod's powerful validation capabilities.