Test Zod schemas comprehensively with unit tests, integration tests, and type tests for validation logic
Test Zod schemas with unit tests, type tests, and integration tests. Use when you need to validate schema transformations, error messages, refinements, and type inference.
/plugin marketplace add djankies/claude-configs/plugin install zod-4@claude-configsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/testing-patterns.mdComprehensive guide to testing Zod v4 schemas, including validation logic, error messages, transformations, and type inference.
For Vitest test structure, mocking, and async patterns, use vitest-4/skills/writing-vitest-tests
import { z } from 'zod';
const userSchema = z.object({
email: z.email().trim().toLowerCase(),
age: z.number().min(18),
username: z.string().trim().min(3)
});
const result = userSchema.safeParse({
email: 'user@example.com',
age: 25,
username: 'john'
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.email).toBe('user@example.com');
}
const invalidResult = userSchema.safeParse({
email: 'not-an-email',
age: 25,
username: 'john'
});
expect(invalidResult.success).toBe(false);
if (!invalidResult.success) {
expect(invalidResult.error.issues[0].path).toEqual(['email']);
}
const emailSchema = z.email().trim().toLowerCase();
const result = emailSchema.safeParse(' USER@EXAMPLE.COM ');
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe('user@example.com');
}
const schema = z.object({
email: z.email({ error: "Please enter a valid email address" }),
password: z.string().min(8, {
error: "Password must be at least 8 characters"
})
});
const result = schema.safeParse({
email: 'invalid',
password: 'password123'
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].message).toBe(
"Please enter a valid email address"
);
}
const passwordSchema = z.string()
.min(8)
.refine(
(password) => /[A-Z]/.test(password),
{ error: "Must contain uppercase letter" }
)
.refine(
(password) => /[0-9]/.test(password),
{ error: "Must contain number" }
);
const validResult = passwordSchema.safeParse('Password123');
expect(validResult.success).toBe(true);
const invalidResult = passwordSchema.safeParse('password123');
expect(invalidResult.success).toBe(false);
if (!invalidResult.success) {
expect(invalidResult.error.issues[0].message).toBe(
"Must contain uppercase letter"
);
}
const emailSchema = z.email().refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
{ error: "Email already exists" }
);
const validResult = await emailSchema.safeParseAsync('new@example.com');
expect(validResult.success).toBe(true);
const invalidResult = await emailSchema.safeParseAsync('existing@example.com');
expect(invalidResult.success).toBe(false);
if (!invalidResult.success) {
expect(invalidResult.error.issues[0].message).toBe("Email already exists");
}
const addressSchema = z.object({
street: z.string().trim().min(1),
city: z.string().trim().min(1),
zip: z.string().trim().regex(/^\d{5}$/)
});
const userSchema = z.object({
name: z.string().trim().min(1),
address: addressSchema
});
const result = userSchema.safeParse({
name: 'John',
address: { street: '123 Main St', city: 'Boston', zip: 'invalid' }
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual(['address', 'zip']);
}
const tagsSchema = z.array(
z.string().trim().min(1)
).min(1, { error: "At least one tag required" });
const result = tagsSchema.safeParse(['valid', '']);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual([1]);
}
const eventSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('click'),
x: z.number(),
y: z.number()
}),
z.object({
type: z.literal('keypress'),
key: z.string()
})
]);
const result = eventSchema.safeParse({
type: 'click',
x: 100,
y: 200
});
expect(result.success).toBe(true);
const userSchema = z.object({
email: z.email(),
age: z.number(),
name: z.string()
});
type User = z.infer<typeof userSchema>;
expectTypeOf<User>().toEqualTypeOf<{
email: string;
age: number;
name: string;
}>();
const schema = z.string().transform(s => parseInt(s));
type Input = z.input<typeof schema>;
type Output = z.output<typeof schema>;
expectTypeOf<Input>().toEqualTypeOf<string>();
expectTypeOf<Output>().toEqualTypeOf<number>();
Always test valid data passes and invalid data fails
Verify trim, lowercase, and other transforms produce expected output
Check custom error messages appear correctly
Handle empty strings, very long strings, special characters
const result = schema.safeParse(data); // ✅
try { schema.parse(data) } // ❌
Verify z.infer, z.input, and z.output produce correct types
Aim for:
For coverage configuration in Vitest 4.x when testing schemas, use vitest-4/skills/configuring-vitest-4 for coverage include patterns and thresholds setup.
Cross-Plugin References: