Nested objects
Real-world data has depth
So far our schemas have been flat: a name, an email, a message, all at the top level. But real-world data almost always has structure. A book has an author (an object). An order has a shipping address (an object). A user has preferences (an object). You need to validate not just the top level, but everything inside it too.
Zod handles this by nesting z.object() inside z.object().
Your first nested schema
import { z } from "zod/v4";
const AddressSchema = z.object({
street: z.string().min(1),
city: z.string().min(1),
state: z.string().length(2),
zip: z.string().regex(/^\d{5}$/),
});
const OrderSchema = z.object({
id: z.string().uuid(),
total: z.number().positive(),
shippingAddress: AddressSchema, // nested object
}); The shippingAddress field is not a primitive. It is an entire AddressSchema object. When you parse an order, Zod validates the top-level fields (id, total) and also dives into the nested address to validate street, city, state, and zip.
What happens when a nested field fails?
OrderSchema.parse({
id: "550e8400-e29b-41d4-a716-446655440000",
total: 29.99,
shippingAddress: {
street: "123 Main St",
city: "Portland",
state: "OR",
zip: "1234", // invalid: must be 5 digits
},
});
// Error path: ["shippingAddress", "zip"]
// Message: "Invalid" Look at that error path: ["shippingAddress", "zip"]. Zod tells you exactly where the problem is. Not just “invalid input” with no context, but the specific field in the specific nested object. The client can point to the exact form field that needs fixing.
Defining nested schemas separately
Notice that we defined AddressSchema as its own variable, then referenced it inside OrderSchema. This is the recommended approach because it lets you reuse the schema:
// src/schemas/address.ts
export const AddressSchema = z.object({
street: z.string().min(1),
city: z.string().min(1),
state: z.string().length(2),
zip: z.string().regex(/^\d{5}$/),
});
// src/schemas/order.ts
import { AddressSchema } from "./address.js";
export const OrderSchema = z.object({
total: z.number().positive(),
shippingAddress: AddressSchema,
billingAddress: AddressSchema.optional(), // reuse!
}); AddressSchema is defined once and used for both shippingAddress (required) and billingAddress (optional). If the address format changes (say you add a country field), you update one schema and both places get the change.
Multiple levels of nesting
You can nest as deep as you need:
const CompanySchema = z.object({
name: z.string().min(1),
address: AddressSchema,
contact: z.object({
name: z.string().min(1),
email: z.string().email(),
phone: z.string().optional(),
}),
}); Notice that contact is defined inline, right inside CompanySchema. That is fine when a nested object is only used in one place. Use separate variables when a schema is reused. Inline when it is not. There is no strict rule here; just keep things readable.
Optional nested objects
Sometimes the entire nested object is optional, not just individual fields within it:
const ProfileSchema = z.object({
name: z.string().min(1),
address: AddressSchema.optional(), // entire nested object is optional
});
ProfileSchema.parse({ name: "Alice" }); // passes: address omitted entirely
ProfileSchema.parse({
name: "Alice",
address: { street: "123 Main", city: "Portland", state: "OR", zip: "97201" },
}); // passes: address provided and valid
ProfileSchema.parse({
name: "Alice",
address: { street: "123 Main" }, // missing city, state, zip
}); // fails: address provided but incomplete This is an important detail. When an optional nested object is provided, it must be fully valid. You cannot send a half-filled address. Either send the complete address or leave it out entirely. This prevents the kind of partial data that causes bugs downstream when you try to format an address and half the fields are missing.
Next, we will tackle arrays, because APIs rarely deal with just one item at a time.
Exercises
Exercise 1: Create a BookSchema with a nested AuthorSchema (name, bio). Parse valid and invalid data. Check the error paths.
Exercise 2: Reuse the same nested schema in two parent schemas. Change the nested schema. Verify both parents reflect the change.
Exercise 3: Make a nested object optional. Parse with the object omitted, with a valid object, and with an invalid object.
What happens when a nested object field fails validation?