Level Up in API Testing: Effortless Schema Validation using Zod!

Azad Husen
4 min readAug 22, 2023
MirageC / Getty Images

Developers have increasingly turned to Zod for its ability to streamline input validation, enhance data structuring, and provide consistent error handling. According to Zod’s official documentation, it’s a TypeScript-first schema declaration and validation library. In this context, the term “schema” is used broadly to encompass a variety of data types, ranging from simple strings to intricate nested objects. My familiarity with Playwright in the context of a blended UI and API testing approach led me to explore Zod. The outcome was mind-blowing, as Zod proved to be an essential tool for seamless schema validation. Now, I am going to share how to use Zod schema validation for API testing, but this time I will be using Reqres simulated environment:

Scenario: In API automation, most tools give a way to check the stuff inside responses. However, I was more interested in how the responses look like, not what is in them. Then the test would be more flexible and won’t easily break when things inside the response change, like when you run tests on different setups or extend tests for different scenarios.

Schema validation ensures that data follows specific patterns, structures, and types you define. It’s like a quality check for your data, catching problems early and stopping errors caused by wrong data types. Strong schema validation doesn’t just make things faster but also reduces the chance of mistakes when making big applications that are ready for real-world use. Here comes Zod into play, let's explore how we can use Zod for schema validation in a Playwright test.

I believe you already have a playwright project, if not follow the steps mentioned in the playwright introduction!
I am going to test this endpoint https://reqres.in/api/users returns a response like this:

JSON Viewer Tree

Here is one test written in Playwright without schema validation for this endpoint:

import { test, expect } from '@playwright/test';

test('GET Users', async ({ request }) => {
const getUsers = await request.get("https://reqres.in/api/users");
expect(getUsers.ok()).toBeTruthy();
const responseJson = await getUsers.json();
expect(responseJson.data[0]).toEqual({
id: 1,
email: "george.bluth@reqres.in",
first_name: "George",
last_name: "Bluth",
avatar: "https://reqres.in/img/faces/1-image.jpg",
});

});

This code takes the JSON response from an API request, and it checks if the data of the first user in the response matches specific expected details like their ID, email, first name, last name, and avatar URL. That sounds like a good plan! Using Zod Schema validation will add an extra layer of robustness to your code by ensuring that the response data conforms to a specific structure or format that you define.

Let's define the user's schema in a separate file which will be imported into the test:

import { z } from "zod";

const userSchema = z.object({
page: z.number().min(1),
per_page: z.literal(6),
total: z.number().min(1),
total_pages: z.number().min(1),
data: z.array(
z.object({
id: z.number(),
email: z.string(),
first_name: z.string(),
last_name: z.string(),
avatar: z.string().url(),
})
),
support: z.object({
url: z.string().url(),
text: z.string(),
}),
});

export default userSchema;

The schema is defined using TypeScript-like syntax provided by Zod, where various validators are applied to different fields to specify their expected types and constraints. For instance, the userSchema defines the structure of the response, including fields like page, per_page, total, data, and support. Each field is validated using Zod's validation methods like z.number(), z.string(), and z.array() with appropriate constraints such as minimum values and URL formats.

Now, import this schema in our test and replace assertions:

import { test, expect } from '@playwright/test';
import userSchema from "../schema";

test('GET Users', async ({ request }) => {
const getUsers = await request.get("https://reqres.in/api/users");
expect(getUsers.ok()).toBeTruthy();
const responseJson = await getUsers.json();
expect(() => userSchema.parse(responseJson)).not.toThrow();

});

userSchema.parse(responseJson): This part is using the userSchema defined earlier to parse and validate the received JSON response. The .parse() function applies the schema rules to the JSON data and validates it against the specified schema.

expect(() => userSchema.parse(responseJson)).not.toThrow();: This part uses expect library along with .not.toThrow() matcher. It verifies that the function inside the arrow function (userSchema.parse(responseJson)) does not throw an error. If the schema validation fails and an error is thrown, the test will fail. If the validation succeeds without errors, the test will pass.

This code could be optimized by adding a playwright custom matcher which is another topic that could be covered in a separate article.

By employing this Zod schema for validation, when the API response is received, the code can ensure that the response data adheres to the specified structure and data types. This adds a layer of robustness to the API testing, as it prevents errors that might arise due to unexpected changes in the response format or data types. Additionally, schema validation helps catch potential issues early on, making it easier to build and maintain reliable API tests that remain resilient to changes in the API’s internals.

Thank you for reading, and I hope you’re utilizing schema validation in your API automation!

--

--