3.8 KiB
3.8 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Use pick() and omit() for Schema Variants | MEDIUM-HIGH | Copying fields between schemas creates duplication; pick/omit derive variants that stay in sync with base schema | object, pick, omit, variants |
Use pick() and omit() for Schema Variants
When you need different views of the same data (public vs private, create vs response), use .pick() and .omit() instead of duplicating fields. This ensures derived schemas stay in sync with the base schema.
Incorrect (duplicating for variants):
import { z } from 'zod'
// Full user schema
const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
passwordHash: z.string(),
name: z.string(),
createdAt: z.date(),
isAdmin: z.boolean(),
})
// Public view - manually duplicated
const publicUserSchema = z.object({
id: z.string().uuid(),
name: z.string(),
// Forgot email - now users can't see it
// Added avatar field - doesn't exist in base schema
avatar: z.string().optional(),
})
// Create input - manually duplicated
const createUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8), // Different from passwordHash
name: z.string(),
// Missing isAdmin - can't set on create? Intentional?
})
Correct (using pick and omit):
import { z } from 'zod'
// Full user schema - single source of truth
const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
passwordHash: z.string(),
name: z.string(),
createdAt: z.date(),
isAdmin: z.boolean(),
})
// Public view - explicitly pick public fields
const publicUserSchema = userSchema.pick({
id: true,
email: true,
name: true,
})
type PublicUser = z.infer<typeof publicUserSchema>
// { id: string; email: string; name: string }
// API response - omit sensitive fields
const userResponseSchema = userSchema.omit({
passwordHash: true,
})
type UserResponse = z.infer<typeof userResponseSchema>
// { id: string; email: string; name: string; createdAt: Date; isAdmin: boolean }
// Create input - omit generated fields
const createUserInputSchema = userSchema
.omit({ id: true, createdAt: true, passwordHash: true })
.extend({
password: z.string().min(8), // Add password (different from hash)
})
type CreateUserInput = z.infer<typeof createUserInputSchema>
// { email: string; name: string; isAdmin: boolean; password: string }
Common patterns:
// Database row → API response (hide internal fields)
const dbRowSchema = z.object({
id: z.number(),
public_id: z.string().uuid(),
email: z.string(),
password_hash: z.string(),
internal_notes: z.string(),
created_at: z.date(),
})
const apiResponseSchema = dbRowSchema.omit({
id: true, // Internal DB id
password_hash: true, // Sensitive
internal_notes: true, // Staff only
})
// Form data → Database insert (add generated fields)
const formSchema = z.object({
title: z.string(),
content: z.string(),
})
const dbInsertSchema = formSchema.extend({
id: z.string().uuid(),
authorId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date(),
})
Chaining operations:
const baseSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
role: z.enum(['admin', 'user']),
secret: z.string(),
})
// Combine pick, omit, partial, extend
const updateSchema = baseSchema
.omit({ id: true, secret: true }) // Remove immutable/sensitive
.partial() // Make all optional for updates
.extend({
updatedAt: z.date().optional(), // Add update timestamp
})
When NOT to use this pattern:
- When derived schemas need different validation rules (not just different fields)
- When the relationship between schemas is not subset/superset
Reference: Zod API - pick/omit