Pick, omit, and extend
The duplication problem
Let’s say you are building a user API. A user has many fields: name, email, age, bio, avatar, role, createdAt. The create endpoint needs name, email, and age (required). The update endpoint needs the same fields but all optional. The response includes everything plus an id.
Without schema composition, you end up writing three nearly identical schemas:
// Lots of duplication
const CreateUser = z.object({ name: z.string(), email: z.string().email(), age: z.number() });
const UpdateUser = z.object({
name: z.string().optional(),
email: z.string().email().optional(),
age: z.number().optional(),
bio: z.string().optional(),
});
const UserResponse = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
age: z.number(),
bio: z.string().optional(),
createdAt: z.string(),
}); Now you decide the email validator needs .toLowerCase(). You have to update three schemas. Forget one and they drift apart. The create schema validates one way, the response schema another. Bugs hide in that drift.
Zod solves this with schema composition: you define a base schema once and derive others from it.
.pick(): select specific fields
.pick() creates a new schema with only the fields you specify:
import { z } from "zod/v4";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().positive(),
bio: z.string().max(500).optional(),
role: z.enum(["user", "admin"]),
createdAt: z.string().datetime(),
});
const CreateUserSchema = UserSchema.pick({ name: true, email: true, age: true });
// Equivalent to: z.object({ name: z.string().min(1), email: z.string().email(), age: z.number().int().positive() }) CreateUserSchema has only name, email, and age, picked from the full UserSchema. All the validators (min(1), email(), int().positive()) come along. You are not redefining them; you are reusing them.
.omit(): exclude specific fields
.omit() is the opposite. It creates a new schema with everything except the fields you specify:
const UserInputSchema = UserSchema.omit({ id: true, createdAt: true, role: true });
// Has: name, email, age, bio — everything except id, createdAt, role When do you pick vs omit? Pick when you want a few fields from many. Omit when you want most fields except a few. Use whichever results in a shorter, more readable call.
.extend(): add fields
.extend() adds new fields to an existing schema:
const UserWithPassword = UserSchema.pick({ name: true, email: true }).extend({
password: z.string().min(8),
confirmPassword: z.string().min(8),
});
// Has: name, email, password, confirmPassword You can also override existing fields with .extend():
const UserWithStringId = UserSchema.extend({
id: z.string(), // override: uuid → plain string
}); Putting it all together: create/update/response
Here is the pattern you will use in every API. A base schema defines all fields. Create, update, and response schemas are all derived from it:
// Base: all fields with their validators
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().trim().min(1).max(100),
email: z.string().trim().toLowerCase().email(),
age: z.number().int().positive().optional(),
bio: z.string().max(500).optional(),
role: z.enum(["user", "admin"]).default("user"),
createdAt: z.string().datetime(),
});
// Create: fields the client must provide
const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true });
// Update: all client fields, but optional (partial update)
const UpdateUserSchema = CreateUserSchema.partial();
// Response: all fields (what the API returns)
type UserResponse = z.infer<typeof UserSchema>; One source of truth. Change the email validator on UserSchema, and it propagates to CreateUserSchema, UpdateUserSchema, and UserResponse automatically. No drift, no forgotten updates.
CreateUserSchema omits id and createdAt because those are generated by the server. UpdateUserSchema uses .partial() (from the Objects lesson) to make every field optional, so the client only sends what they want to change. The UserResponse type is inferred directly from the base schema.
This is exactly how production APIs handle schema management. One base, many derived schemas, zero duplication.
Next, we will look at merging and intersecting schemas, which is how you combine two independent schemas into one.
Exercises
Exercise 1: Create a full BookSchema. Derive CreateBookSchema (omit id, createdAt) and UpdateBookSchema (partial of CreateBookSchema).
Exercise 2: Use .pick() to create a BookSummarySchema with only id, title, and author.
Exercise 3: Use .extend() to add a password field to a user creation schema.
Why derive create/update schemas from a base schema instead of writing them separately?