How to Validate Map and Set Collections with Zod: Complete Guide

Zod provides dedicated z.map() and z.set() schemas that validate JavaScript Map and Set collections by specifying schemas for keys and values (Map) or values only (Set), with built-in support for size constraints, asynchronous refinements, and immutable read-only outputs.

The colinhacks/zod library (v4) offers first-class schemas for validating JavaScript's built-in collection types. Whether you are enforcing type safety on entries in a Map<string, number> or ensuring a Set contains unique validated objects, Zod's map and set validation APIs handle runtime type checking, size constraints, and complex async validation workflows.

Creating Map Schemas in Zod

Use the z.map() factory function to create a schema that validates both the keys and values of a JavaScript Map. This function requires two arguments: a schema for the key type and a schema for the value type.

Basic Map Validation

The factory for Map schemas resides in [packages/zod/src/v4/mini/schemas.ts](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/mini/schemas.ts#L1212-L1224). You can define typed Maps for any combination of primitive or complex schemas:

import { z } from "zod/v4";

// Validates Map<string, number>
const StringNumberMap = z.map(z.string(), z.number());

// Validates Map<string, { name: string }>
const UserMap = z.map(
  z.string(),
  z.object({ name: z.string() })
);

Size Constraints and Non-Empty Requirements

Both Map and Set schemas expose chainable methods to enforce collection dimensions. These constraints generate issues with codes too_small or too_big when violated:

const SmallMap = StringNumberMap.max(5);    // Maximum 5 entries
const LargeMap = StringNumberMap.min(2);    // Minimum 2 entries
const ExactMap = StringNumberMap.size(3);   // Exactly 3 entries
const NonEmptyMap = StringNumberMap.nonempty(); // At least 1 entry

Parsing and Validating Map Data

The core parsing logic for Maps, including type checking, element validation, and async handling, is implemented in [packages/zod/src/v4/core/schemas.ts](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts#L2886-L2944).

Synchronous Parsing

Use .safeParse() to validate a Map instance against your schema. The method returns a discriminated union with success boolean and either data or error:

const result = StringNumberMap.safeParse(
  new Map([
    ["age", 30],
    ["year", 2024],
  ])
);

if (result.success) {
  // result.data is typed as Map<string, number>
  console.log(result.data);
} else {
  console.error(result.error.format());
}

For comprehensive test cases covering parsing behaviors, see [packages/zod/src/v4/classic/tests/map.test.ts](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/tests/map.test.ts).

Async Validation with Refinements

Map schemas support asynchronous refinements on both keys and values. When parsing with .safeParseAsync(), Zod detects promises returned by element schemas and aggregates them using Promise.all before finalizing the payload (see the for … loop implementation in [core/schemas.ts](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts#L2927-L2944)):

const AsyncMap = z.map(
  z.string().refine(async (v) => v.startsWith("id_"), "invalid key"),
  z.number().refine(async (n) => n > 0, "must be positive")
);

await AsyncMap.safeParseAsync(
  new Map([["id_123", 10]]) // ❌ fails both refinements if conditions not met
);

Creating Set Schemas in Zod

Sets are validated using z.set(), which accepts a single schema describing the set's element type. The factory is defined in [packages/zod/src/v4/mini/schemas.ts](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/mini/schemas.ts#L1236-L1243).

Basic Set Validation

// Validates Set<boolean>
const BoolSet = z.set(z.boolean());

// Validates Set<string> with email format
const EmailSet = z.set(z.string().email());

Size Constraints

Set schemas support the same size helpers as Maps, issuing too_small or too_big errors when constraints are violated:

const AtLeastTwo = BoolSet.min(2);
const AtMostFive = BoolSet.max(5);
const ExactlyThree = BoolSet.size(3);
const NonEmptySet = BoolSet.nonempty();

Parsing Sets and Handling Validation Errors

The core Set parser, handling iteration, type checks, and error creation, lives in [packages/zod/src/v4/core/schemas.ts](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts#L2950-L2988).

const result = BoolSet.safeParse(new Set([true, false]));

if (result.success) {
  // result.data is Set<boolean>
  console.log([...result.data]); // [true, false]
}

Zod emits specific issue codes for common validation failures:

  • invalid_type: The input is not a Map or Set instance.
  • invalid_key: A Map key fails its schema (e.g., non-primitive object used as key when string expected).
  • too_small: Collection size is below the .min() or .nonempty() threshold.
  • too_big: Collection size exceeds the .max() limit.

Enforcing Collection Immutability with Readonly

Both ZodMap and ZodSet schemas expose a .readonly() method that returns a schema producing frozen collections. When parsed, the resulting Map or Set has Object.isFrozen(value) === true, guaranteeing immutability after validation. This is implemented via the $ZodMap and $ZodSet constructors in [core/schemas.ts](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts):

const ReadOnlyMap = StringNumberMap.readonly();
const parsed = ReadOnlyMap.parse(new Map([["a", 1]]));

console.log(Object.isFrozen(parsed)); // true
// parsed.set("b", 2); // TypeError: Cannot add property

Summary

  • Map schemas are created with z.map(keySchema, valueSchema) and Set schemas with z.set(elementSchema), both defined in mini/schemas.ts.
  • Size constraints use chainable methods .min(), .max(), .size(), and .nonempty() to enforce collection dimensions, generating too_small or too_big issues.
  • Async validation is supported via .safeParseAsync(), with Zod aggregating promises from element schemas using Promise.all before returning results.
  • Read-only collections enforce runtime immutability through the .readonly() method, which freezes the parsed output.
  • Error specificity includes distinct codes for type mismatches (invalid_type), invalid Map keys (invalid_key), and size violations.

Frequently Asked Questions

How do I validate that a Map contains specific required keys?

Zod's z.map() validates the type of keys and values but does not enforce the presence of specific keys by default. To require specific keys like "id" or "name", use .refine() on the Map schema to check map.has("id"), or consider using z.record() or z.object() if the key structure is fixed and known.

Can I use objects as keys in a Zod-validated Map?

No. Zod treats non-primitive keys (objects or arrays) as invalid and will emit an issue with code: "invalid_key". The validation logic in core/schemas.ts explicitly checks for primitive key types. Use strings, numbers, or symbols as Map keys when working with Zod schemas.

What is the difference between .nonempty() and .min(1) for Sets and Maps?

Functionally, .nonempty() and .min(1) produce identical validation results for both Map and Set schemas, ensuring the collection contains at least one entry. However, .nonempty() provides clearer semantic intent in your schema definitions, indicating that the collection must not be empty rather than merely meeting a numeric minimum.

How does Zod handle async validation for Maps with multiple entries?

When parsing a Map with asynchronous refinements on keys or values, Zod collects all promises returned during the iteration over Map entries. It then executes Promise.all on these promises (as seen in [core/schemas.ts](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts#L2927-L2944)) before finalizing the validation result, ensuring all async checks complete before determining success or aggregating errors.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →