Testing Zod Schemas
Why test schemas
Zod schemas define what your API accepts. If a schema is wrong — allows invalid data or rejects valid data — the bug affects every request. Testing schemas ensures the contract is correct.
Testing valid input
// tests/unit/schemas.test.ts
import { describe, test, expect } from "vitest";
import { CreateBookV2 } from "../../src/v2/schemas.js";
describe("CreateBookV2", () => {
const validBook = {
title: "Kindred",
authorId: "550e8400-e29b-41d4-a716-446655440000",
genre: "science-fiction",
};
test("accepts valid input", () => {
const result = CreateBookV2.safeParse(validBook);
expect(result.success).toBe(true);
});
test("accepts valid input with optional description", () => {
const result = CreateBookV2.safeParse({ ...validBook, description: "A great book" });
expect(result.success).toBe(true);
});
test("accepts valid input without optional fields", () => {
const result = CreateBookV2.safeParse(validBook);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.description).toBeUndefined();
}
});
}); [!NOTE] Always use
safeParsein tests, notparse.safeParsereturns a result object — your test checksresult.success.parsethrows, which makes the test harder to assert against and produces less readable test output.
Testing invalid input
describe("CreateBookV2 rejection", () => {
test("rejects missing title", () => {
const result = CreateBookV2.safeParse({
authorId: "550e8400-e29b-41d4-a716-446655440000",
genre: "fiction",
});
expect(result.success).toBe(false);
});
test("rejects empty title", () => {
const result = CreateBookV2.safeParse({
title: "",
authorId: "550e8400-e29b-41d4-a716-446655440000",
genre: "fiction",
});
expect(result.success).toBe(false);
});
test("rejects invalid authorId (not a UUID)", () => {
const result = CreateBookV2.safeParse({
title: "Kindred",
authorId: "not-a-uuid",
genre: "fiction",
});
expect(result.success).toBe(false);
});
test("rejects invalid genre (not in enum)", () => {
const result = CreateBookV2.safeParse({
title: "Kindred",
authorId: "550e8400-e29b-41d4-a716-446655440000",
genre: "romance",
});
expect(result.success).toBe(false);
});
}); Testing edge cases
describe("CreateBookV2 edge cases", () => {
test("trims whitespace from title", () => {
const result = CreateBookV2.safeParse({
title: " Kindred ",
authorId: "550e8400-e29b-41d4-a716-446655440000",
genre: "fiction",
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.title).toBe("Kindred");
}
});
test("rejects whitespace-only title", () => {
const result = CreateBookV2.safeParse({
title: " ",
authorId: "550e8400-e29b-41d4-a716-446655440000",
genre: "fiction",
});
expect(result.success).toBe(false);
});
test("rejects title exceeding max length", () => {
const result = CreateBookV2.safeParse({
title: "A".repeat(201),
authorId: "550e8400-e29b-41d4-a716-446655440000",
genre: "fiction",
});
expect(result.success).toBe(false);
});
test("strips extra fields", () => {
const result = CreateBookV2.safeParse({
title: "Kindred",
authorId: "550e8400-e29b-41d4-a716-446655440000",
genre: "fiction",
extraField: "should be stripped",
});
expect(result.success).toBe(true);
if (result.success) {
expect((result.data as any).extraField).toBeUndefined();
}
});
}); Testing query schemas with coercion
import { BookQueryV2 } from "../../src/v2/schemas.js";
describe("BookQueryV2", () => {
test("coerces string page to number", () => {
const result = BookQueryV2.safeParse({ page: "2" });
expect(result.success).toBe(true);
if (result.success) expect(result.data.page).toBe(2);
});
test("applies defaults for missing fields", () => {
const result = BookQueryV2.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.page).toBe(1);
expect(result.data.limit).toBe(20);
expect(result.data.sort).toBe("title");
}
});
test("rejects page below 1", () => {
const result = BookQueryV2.safeParse({ page: "0" });
expect(result.success).toBe(false);
});
test("rejects limit above 100", () => {
const result = BookQueryV2.safeParse({ limit: "200" });
expect(result.success).toBe(false);
});
}); Exercises
Exercise 1: Write tests for a ContactSchema: valid input, missing required fields, invalid email, whitespace-only name.
Exercise 2: Test a query schema with coercion. Verify strings are converted to numbers, defaults are applied.
Exercise 3: Test that extra fields are stripped by default. Test that .strict() rejects them.
Why use safeParse instead of parse in tests?