Playwright·Cookbook Field Manual
Part 01 · Network Mocking Card 08

Validate with Zod Schemas

Card 08: Validate with Zod Schemas

What This Pattern Solves

TypeScript types help at compile time, but don’t validate runtime data. When API contracts change, fixtures drift, or mocks have typos, you need runtime validation that fails tests immediately with clear error messages. Zod provides schemas that both validate and type your data.

How It Works

  1. Define a Zod schema describing the expected API response structure
  2. Derive TypeScript types from the schema using z.infer<>
  3. In tests, parse mock/fixture data with schema.parse()
  4. If data doesn’t match schema, Zod throws with detailed error
  5. Successful parse gives you fully-typed data for assertions

This catches contract drift before assertions even run.

Code Example

import { test, expect } from '@playwright/test';
import { SwapiPersonSchema } from '../swapi/schema';
import type { SwapiPerson } from '../swapi/schema';

// Schema (shared across tests) — src/swapi/schema.ts
// export const SwapiPersonSchema = z.object({
//   name: z.string(),
//   height: z.string(),
//   mass: z.string(),
//   url: z.string().url(),
//   films: z.array(z.string().url()),
// });
// export type SwapiPerson = z.infer<typeof SwapiPersonSchema>;

const knownGoodPerson: SwapiPerson = {
  name: 'Luke Skywalker',
  height: '172',
  mass: '77',
  url: 'https://swapi.dev/api/people/1/',
  films: [],
};

test.describe('08-validate-with-zod: Validate response with Zod', () => {
  test('schema gates the boundary; only parsed data reaches the page', async ({ page }) => {
    // Parse at the boundary — only validated data is fulfilled.
    await page.route('**/swapi.dev/api/people/1/**', (route) =>
      route.fulfill({ json: SwapiPersonSchema.parse(knownGoodPerson) }),
    );

    await page.goto('/cards/08');

    await expect(page.getByTestId('person-name')).toHaveText('Luke Skywalker');
  });
});

Run This Example

pnpm test src/08-validate-with-zod

Prerequisites

Key Concepts

When to Use This Pattern

Common Mistakes

  1. Not handling parse errors:

    // ❌ WRONG - parse throws, test crashes with cryptic error
    const person = SwapiPersonSchema.parse(maybeInvalidData);
    
    // ✓ CORRECT - catch and provide context
    try {
      const person = SwapiPersonSchema.parse(mockData);
    } catch (error) {
      throw new Error(`Mock data invalid: ${error}`);
    }
    
    // ✓ BETTER - use safeParse for custom handling
    const result = SwapiPersonSchema.safeParse(mockData);
    if (!result.success) {
      console.error('Validation failed:', result.error.format());
    }
  2. Validating in wrong place:

    // ❌ WRONG - validating after page loaded (too late)
    await page.goto('/');
    const person = SwapiPersonSchema.parse(mockData);
    
    // ✓ CORRECT - validate before fulfill
    const person = SwapiPersonSchema.parse(mockData);
    await page.route('**/*', (route) =>
      route.fulfill({ body: JSON.stringify(person) })
    );
  3. Schema too strict (brittle tests):

    • Only validate fields you actually use
    • Use .passthrough() to allow extra fields
    • Consider .partial() for optional fields
  4. Not reusing schemas:

    // ❌ WRONG - schema defined per test
    test('...', () => {
      const schema = z.object({ name: z.string() });
    });
    
    // ✓ CORRECT - shared schema file
    // src/schemas/swapi.ts
    export const SwapiPersonSchema = z.object({...});

Flow Diagram

flowchart TB
    MockData[Mock/Fixture Data]
    Schema[Zod Schema Definition]
    Parse[SwapiPersonSchema.parse]
    Valid{Valid?}
    Typed[Typed Person Object]
    TestFail[❌ Test fails with Zod error]
    Route[route.fulfill]
    Assertions[Assertions with type safety]

    MockData --> Parse
    Schema --> Parse
    Parse --> Valid

    Valid -->|Yes| Typed
    Valid -->|No| TestFail

    Typed --> Route
    Route --> Assertions

Live Demo

👇 Test validates API responses with Zod:

Loading…

Run This Example

pnpm test src/08-validate-with-zod