| name | reviewing-patterns |
| description | Review Zod schemas for correctness, performance, and v4 best practices Use when this capability is needed. |
| metadata | {"author":"djankies"} |
Reviewing Zod Schemas
Purpose
Comprehensive code review checklist for Zod schemas ensuring v4 compliance, type safety, performance, and maintainability.
Review Checklist
1. API Compatibility
Check for Deprecated v3 Patterns
String format methods:
z.string().email()
z.string().uuid()
z.string().datetime()
z.email()
z.uuid()
z.iso.datetime()
Error customization:
z.string({ message: "Required" })
z.string({ invalid_type_error: "Wrong type" })
z.string({ required_error: "Missing" })
z.string({ error: "Required" })
Schema composition:
schemaA.merge(schemaB)
schemaA.extend(schemaB.shape)
2. String Transformations
Check for Missing Transformations
User input should always be trimmed:
z.string()
z.string().trim()
Email and username normalization:
z.email()
z.email().trim().toLowerCase()
Code/identifier normalization:
z.string()
z.string().trim().toUpperCase()
Manual vs. declarative transformations:
const trimmed = input.trim();
z.string().parse(trimmed);
z.string().trim().parse(input);
3. Type Safety
Type Inference
Check proper type extraction:
type User = typeof userSchema;
type User = z.infer<typeof userSchema>;
Transform pipelines:
const schema = z.string().transform(s => parseInt(s));
type Input = z.input<typeof schema>;
type Output = z.output<typeof schema>;
Branded types for nominal typing:
const userId = z.string();
const userId = z.string().brand<'UserId'>();
4. Error Handling
Parse vs. SafeParse
Anti-pattern:
try {
const data = schema.parse(input);
} catch (error) {
console.error(error);
}
Best practice:
const result = schema.safeParse(input);
if (!result.success) {
console.error(result.error.flatten());
return;
}
const data = result.data;
Error Messages
Check for user-friendly errors:
z.string()
z.string({ error: "Name is required" })
Complex error maps:
const userSchema = z.object({
email: z.email({ error: "Please enter a valid email address" }),
age: z.number({ error: "Age must be a number" }).min(18, {
error: "Must be 18 or older"
})
});
5. Performance
Schema Reuse
Anti-pattern:
function validateUser(data: unknown) {
const schema = z.object({ email: z.email() });
return schema.parse(data);
}
Best practice:
const userSchema = z.object({ email: z.email() });
function validateUser(data: unknown) {
return userSchema.parse(data);
}
Array Validation
Anti-pattern:
items.forEach(item => schema.parse(item));
Best practice:
const arraySchema = z.array(schema);
arraySchema.parse(items);
Zod v4 bulk validation is 7x faster than item-by-item parsing.
Passthrough vs. Strict
Understand cost implications:
z.object({}).strict()
z.object({}).passthrough()
Use .passthrough() when you don't need to strip unknown properties.
6. Schema Design
Appropriate Schema Types
Check for correct schema selection:
z.string().refine(s => s === 'true' || s === 'false')
z.stringbool()
Union vs. Discriminated Union:
z.union([typeA, typeB])
z.discriminatedUnion('type', [
z.object({ type: z.literal('a'), ...typeA }),
z.object({ type: z.literal('b'), ...typeB })
])
Optional vs. Nullable vs. Nullish:
z.string().optional()
z.string().nullable()
z.string().nullish()
Use the most specific type for your use case.
Schema Composition
Extend for adding fields:
const base = z.object({ id: z.string() });
const extended = base.extend({
name: z.string(),
email: z.email()
});
Pick/Omit for subsets:
const userSchema = z.object({
id: z.string(),
email: z.email(),
password: z.string()
});
const publicUserSchema = userSchema.omit({ password: true });
const loginSchema = userSchema.pick({ email: true, password: true });
7. Validation Logic
Refinements
Check refinement usage:
z.string().refine(val => val.length > 5)
z.string().refine(
val => val.length > 5,
{ error: "Must be at least 6 characters" }
)
Async refinements:
z.string().email().refine(
async (email) => {
return await checkEmailUnique(email);
},
{ error: "Email already exists" }
)
Transformations
Check transformation order:
z.string().transform(s => s.toUpperCase()).trim()
z.string().trim().transform(s => s.toUpperCase())
Transformations run after validation, built-in methods run first.
Type-safe transformations:
const schema = z.string().transform(s => parseInt(s));
type Output = z.output<typeof schema>;
8. Security
Input Validation
Check entry point validation:
function handleFormSubmit(formData: FormData) {
const data = {
email: formData.get('email'),
password: formData.get('password')
};
const result = loginSchema.safeParse(data);
if (!result.success) {
return { errors: result.error };
}
await login(result.data);
}
Avoid validation bypass:
function processUser(user: User) {
await saveToDb(user);
}
processUser({ email: 'test@example.com' });
Always validate at boundaries:
function processUser(data: unknown) {
const result = userSchema.safeParse(data);
if (!result.success) throw new Error('Invalid user');
await saveToDb(result.data);
}
SQL Injection Prevention
Zod validates type/shape, NOT malicious content:
z.string()
Combine with sanitization:
import { sanitize } from 'sanitization-library';
const schema = z.string().transform(s => sanitize(s));
9. Testing
Schema Tests
Test valid inputs:
describe('userSchema', () => {
it('accepts valid user', () => {
const result = userSchema.safeParse({
email: 'test@example.com',
username: 'john'
});
expect(result.success).toBe(true);
});
});
Test invalid inputs:
it('rejects invalid email', () => {
const result = userSchema.safeParse({
email: 'not-an-email',
username: 'john'
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual(['email']);
}
});
Test transformations:
it('trims and lowercases email', () => {
const schema = z.email().trim().toLowerCase();
const result = schema.safeParse(' TEST@EXAMPLE.COM ');
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe('test@example.com');
}
});
Review Process
Step 1: Automated Checks
Run validation skill:
/review zod-compatibility
Checks for:
- Deprecated v3 APIs
- Missing string transformations
- Parse + try/catch anti-patterns
Step 2: Manual Review
Review each schema for:
- Appropriate schema types
- Error message quality
- Performance optimizations
- Type safety
- Security considerations
Step 3: Test Coverage
Ensure tests cover:
- Valid input scenarios
- Invalid input scenarios
- Edge cases
- Transformation behavior
- Error message correctness
Common Issues
Issue 1: Missing Trim on User Input
Problem: Whitespace causes validation failures
Fix:
z.string().min(1)
z.string().trim().min(1)
Issue 2: Not Using SafeParse
Problem: Exceptions for invalid data
Fix:
try { schema.parse(data) }
const result = schema.safeParse(data)
Issue 3: Schema Defined Inside Function
Problem: Schema recreated on every call
Fix:
function validate(data: unknown) {
const schema = z.object({...});
return schema.parse(data);
}
const schema = z.object({...});
function validate(data: unknown) {
return schema.parse(data);
}
Issue 4: Wrong Type Extraction
Problem: Using schema type directly
Fix:
type User = typeof userSchema;
type User = z.infer<typeof userSchema>;
Integration with Review Plugin
This skill integrates with the review plugin via review: true frontmatter.
Invoke with:
/review zod
Or explicitly:
/review zod-schemas
References
- Compatibility validation: Use the validating-schema-basics skill from the zod-4 plugin
- v4 Features: Use the validating-string-formats skill from the zod-4 plugin
- Error handling: Use the customizing-errors skill from the zod-4 plugin
- Performance: Use the optimizing-performance skill from the zod-4 plugin
- Testing: Use the testing-zod-schemas skill from the zod-4 plugin
Cross-Plugin References:
- If reviewing TypeScript type safety alongside Zod schemas, use the reviewing-type-safety skill for comprehensive type validation patterns
- If reviewing security concerns, use the reviewing-security skill for automated vulnerability scanning patterns
Success Criteria
- ✅ No deprecated v3 APIs
- ✅ String transformations used appropriately
- ✅ safeParse pattern for error handling
- ✅ Schema defined at module level
- ✅ User-friendly error messages
- ✅ Type inference correct
- ✅ Comprehensive test coverage
- ✅ Security validated at entry points
Converted and distributed by TomeVault — claim your Tome and manage your conversions.