hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Validation for beginners with Zod

What is Zod

  • The problem with untyped data
  • Your first schema
  • Project setup

Primitive types

  • Strings
  • Numbers
  • Booleans, dates, and literals

Objects and arrays

  • Objects
  • Nested objects
  • Arrays
  • Unions and discriminated unions

Transforms and refinements

  • Transforms
  • Refinements
  • Preprocessing and coercion

Composing schemas

  • Pick, omit, and extend
  • Merge and intersection
  • Reusable schemas

Zod in practice

  • Validating API requests
  • Error formatting
  • Inferring TypeScript types
  • Cheatsheet and capstone

Arrays

Lists of things

APIs deal with lists all the time. A list of tags on a blog post. A list of items in an order. A list of email addresses to notify. We know how to validate individual values and objects, but what about a collection of them?

z.array() wraps any schema to validate an array where every element matches that schema.

Basic arrays

import { z } from "zod/v4";

z.array(z.string()); // array of strings
z.array(z.number()); // array of numbers
z.array(z.string().email()); // array of valid emails

Let’s see what happens with different inputs:

z.array(z.string()).parse(["a", "b", "c"]); // returns ["a", "b", "c"]
z.array(z.string()).parse(["a", 42, "c"]); // fails: element at index 1 is not a string
z.array(z.string()).parse("not an array"); // fails: "Expected array, received string"
z.array(z.string()).parse([]); // passes: empty array is valid

Notice that an empty array passes by default. Sometimes that is fine (a post with no tags). Sometimes it is not (an order with no items). Constraints let you control this.

Array constraints

z.array(z.string()).min(1); // at least 1 element (not empty)
z.array(z.string()).max(10); // at most 10 elements
z.array(z.string()).length(3); // exactly 3 elements
z.array(z.string()).nonempty(); // same as min(1) — at least 1 element
z.array(z.string()).nonempty().parse([]); // fails: "Array must contain at least 1 element(s)"
z.array(z.string()).nonempty().parse(["hello"]); // passes
z.array(z.string()).max(2).parse(["a", "b", "c"]); // fails: "Array must contain at most 2 element(s)"

.nonempty() and .min(1) do the same thing. Use whichever reads better to you.

Arrays of objects

This is the most common pattern in API development: an array of structured items where each item has to be valid.

const TagSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
});

const BookSchema = z.object({
  title: z.string().min(1),
  tags: z.array(TagSchema).min(1).max(10), // 1-10 tags, each validated
});

Every element in the tags array gets validated against TagSchema. If any element fails, the error path includes the array index so you know exactly which item has the problem:

BookSchema.parse({
  title: "Kindred",
  tags: [
    { id: "550e8400-...", name: "sci-fi" },
    { id: "not-a-uuid", name: "fiction" }, // invalid id
  ],
});
// Error path: ["tags", 1, "id"] — element at index 1, field "id"

The path ["tags", 1, "id"] tells you: the tags array, at index 1, the id field is invalid. A frontend could highlight exactly the right input in a dynamic list of tags. This is how production apps surface validation errors on repeated form fields.

Nested arrays

Arrays can contain arrays, if your data has that shape:

const MatrixSchema = z.array(z.array(z.number()));

MatrixSchema.parse([
  [1, 2],
  [3, 4],
]); // passes
MatrixSchema.parse([[1, "2"]]); // fails: "Expected number, received string" at [0][1]

You probably will not need this often, but it is good to know the nesting works at any depth.

Applying to the contact form

We could extend our contact form to support optional tags:

const ContactSchema = z.object({
  name: z.string().trim().min(1).max(100),
  email: z.string().trim().toLowerCase().email(),
  phone: z.string().trim().min(7).max(20).optional(),
  subject: z.enum(["general", "support", "sales", "feedback"]),
  message: z.string().trim().min(10).max(5000),
  tags: z.array(z.string().min(1)).max(5).optional(), // up to 5 tags, optional
});

The tags field is optional (you do not have to send it), but if you do, it must be an array of non-empty strings with a maximum of 5 elements. Each element is individually validated.

Next up: unions and discriminated unions. Sometimes a value can be one of several different shapes, and you need Zod to figure out which one it is.

Exercises

Exercise 1: Create a schema for an array of emails. Parse ["[email protected]", "invalid"]. Check the error path for the invalid element.

Exercise 2: Create a schema for an array of objects (items in an order). Validate that each item has a productId and quantity.

Exercise 3: Use .nonempty() on an array. Parse an empty array and verify it fails.

How does Zod report errors for invalid elements in an array?

← Nested objects Unions and discriminated unions →

© 2026 hectoday. All rights reserved.