CYPRESS-AJV-SCHEMA-VALIDATOR Plugin: The Brave Vigilante for Your API Contracts
Tags: cypress, qa, automation, testing
Published: 2024-06-15 22:47:53

**Seamlessly Validate Your APIs Against Well-Documented Schemas from Swagger and Other API Tools and Standards**

_(Cover image from pexels.com by Picography)_

---

- [ACT 1: EXPOSITION](#act-1-exposition)
- [ACT 2: CONFRONTATION](#act-2-confrontation)
  - [Contract Testing: Laying Down the Law](#contract-testing-laying-down-the-law)
  - [About JSON Schemas and Schema Documents: The Rulebook](#about-json-schemas-and-schema-documents-the-rulebook)
  - [The Trusty Sidekick: The Ajv JSON Schema Validator](#the-trusty-sidekick-the-ajv-json-schema-validator)
  - [The new Vigilante on the block: The CYPRESS-AJV-SCHEMA-VALIDATOR Plugin](#the-new-vigilante-on-the-block-the-cypress-ajv-schema-validator-plugin)
- [ACT3: RESOLUTION](#act3-resolution)

---

### ACT 1: EXPOSITION

Imagine you hire a contractor to renovate your home. You agree on work and price, documenting everything in a **contract**. But the contractor uses lower-quality materials and skips renovations. The contract is breached.

What do you do? Obviously, you need a brave fighter for justice—a fearless vigilante—to put that unscrupulous individual in their place. 🙂

Now, think of your API. The **API schema** is your contract. If the backend delivers unexpected data, you need a competent guardian to fight for your rights and point out all those unjust violations that blindsided you. That’s where a **Schema Validator** comes in to help you enforce your rights.

When I was creating my first API tests, unfortunately, many of the tests suddenly failed from one day to the next. Why now? I had to dig through those tests, debugging them and figuring out what could have gone wrong: Was it the test? Was the app changed? Was the data changed?

In my opinion, **one of the most common reasons why web applications fail (if not the most common) is due to unexpected, uninformed, or undocumented changes in the API**. Quite often, the 'QA guy' is the last to know about these changes; however, he is the one who has to sign off on the release. Counterintuitive, right?

That's why API testing exists alongside end-to-end (E2E) testing. One of the most common practices (again... if not the most common) when testing a specific API endpoint is to include assertions over the response to verify that the data obtained has a certain structure or design. Assertions like: this property is of type string, or that other property is an array of integers, the value is not null and is within a min and max range, or the property is present in the response, and so on.

Consequently, our tests for validating the correctness of the data structure in the response end up having quite a large number of lines of code, which we need to change when there are changes in the API. If you have, let's say, 100 tests just for one of your APIs, the time spent on maintenance will be considerable, especially if the API specification is still under development.

However, numerous organizations have well-documented backend APIs using tools like Swagger, following well-defined standard formats such as OpenAPI. Many of these organizations also do a fairly good job of updating these schemas regularly.

**So why don't we use these schemas to validate the contract between our backend services and our frontend application? We can use these Schema Validators in our API tests to monitor and flag any injustice inflicted on our beloved API—just like a brave Vigilante.**

---

### ACT 2: CONFRONTATION

#### Contract Testing: Laying Down the Law

But before we get too far ahead of ourselves, let's understand a little bit about what **_Contract Testing_** is.

Contract Testing is an approach in software testing where the interactions between different services are tested based on the contract they agree upon. A contract specifies the expected inputs and outputs for a service, ensuring that different components interact correctly and reliably.

There are two main approaches to contract testing: Consumer-Driven Contract Testing and Producer-Driven Contract Testing. The key difference between them lies in who defines the contract.

- **Consumer-Driven Contract Testing** focuses on the needs of the consumer. The contract is defined by the consumers of the API, dictating what they expect from the provider. One of the most popular tools for this approach is Pact. Pact, and specifically PactJS, is a powerful tool that allows consumers to create contracts that providers can then verify, ensuring that their APIs meet consumer expectations.

- **Producer-Driven Contract Testing**, on the other hand, is where the provider defines the contract. This approach is often centered around schema validation. Producers outline the structure and constraints of the data they will provide, ensuring that consumers handle it correctly. Schema validation tools, such as Ajv, are crucial in this approach as they verify that the responses adhere to the predefined schema.

By leveraging these two methods, teams can ensure more robust and reliable communication between API producers and consumers, significantly reducing the risk of integration issues.

&nbsp;  

#### About JSON Schemas and Schema Documents: The Rulebook

Let's clarify a couple of key components that we'll be discussing throughout this post:

- **JSON Schema**: It is a hierarchical, declarative language that describes and validates JSON data.

```json
{
  "title": "User",
  "type": "object",
  "properties": {
    "id": {
      "type": "integer"
    },
    "name": {
      "type": "string"
    },
    "address": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string"
        },
        "zipCode": {
          "type": "string"
        }
      },
      "required": ["city", "zipCode"]
    }
  },
  "required": ["id", "name", "address"]
}
```

- **OpenAPI 3.0.1 and Swagger 2.0 Schema Documents**: The OpenAPI Specification (formerly Swagger Specification) are schema documents to describe your entire API (in JSON format or XML format). So a schema document will contain multiple schemas, one for each supported combination of _Endpoint - Method - Expected Response Status_ (also called _path_) by that API.

```json
{
  "openapi": "3.0.1",
  "info": {
    "title": "User API",
    "version": "1.0.0"
  },
  "paths": {
    "/users/{id}": {
      "get": {
        "summary": "Get a user by ID",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "User details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "User": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "address": {
            "type": "object",
            "properties": {
              "city": {
                "type": "string"
              },
              "zipCode": {
                "type": "string"
              }
            },
            "required": ["city", "zipCode"]
          }
        },
        "required": ["id", "name", "address"]
      }
    }
  }
}
```

For the JSON above, an example of path would be: **"/users/{id}**" - "**get**" - "**200**".

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bah2g5vjhj5s2g9p97eg.png)

&nbsp;  

#### The Trusty Sidekick: The Ajv JSON Schema Validator

AJV (Another JSON Schema Validator) is a JavaScript library that validates data objects against a JSON Schema structure ([Ajv official website](https://ajv.js.org/)).

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9034iws2f00ybdejc86a.png)

Don’t be fooled by its name—it’s not just another schema validator. AJV is an fantastic tool!

In my opinion, **AJV** is a very **versatile**, **powerful**, **fast**, and **well-maintained** JSON Schema validator. On top of that, it has **excellent documentation**.

But no matter how amazing a Vigilante might be, they all have their weaknesses.

##### Setbacks I Experienced Using Ajv Schema Validator Out of the Box

Although Ajv is a plugin that's fairly easy to integrate into your Cypress frameworks, when I implemented schema validation for OpenAPI and Swagger using Ajv for a large number of APIs across multiple Cypress projects, I encountered a few setbacks:

1. The APIs to test were documented with the Swagger tool in OpenAPI 3.0.1 or Swagger 2.0 schema documents, each containing the full specification of the API (endpoints, methods, expected result statuses). However, Ajv requires the specific JSON schema object of the response to validate, so you cannot feed it a full schema document with the entire API definition.

    As a result, I found myself repeatedly writing the same custom code and commands across all API test projects to: first, extract the specific schema to validate from the full schema document; and second, "massage" that schema into a format that Ajv could process.

    Obviously, this is not very DRY (Don’t Repeat Yourself) principle-friendly!

2. I discovered that the way Ajv returns schema validation errors is not straightforward. Many of these errors are particularly complex to understand.

    If you don't believe me, check out this example of schema errors provided by Ajv for a relatively simple schema with just two objects in the data response:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hbvrf5b171mxwe2o9fhi.png)

Ugh! Right?

So, **what if we take the Ajv Schema Validator to the next level by integrating it seamlessly with Cypress**, and providing the schema discrepancies results in a very user-friendly way that is easy to understand? This would enable easier and more robust validation in our testing workflow.

&nbsp;  

#### The new Vigilante on the block: The CYPRESS-AJV-SCHEMA-VALIDATOR Plugin

Afterward, it became clear to me that all of this could be turned into a plugin. However, I wanted it to be incredibly easy to use, requiring virtually no configuration.

The new `cypress-ajv-schema-validator` plugin is available for installation on [NPM](https://www.npmjs.com/package/cypress-ajv-schema-validator), and the open source code is available on [GitHub](https://github.com/sclavijosuero/cypress-ajv-schema-validator).

##### Main Features

- It includes a Cypress command **`cy.validateSchema()`** and a utility function **`validateSchema()`** to report JSON Schema validation errors in the response obtained from any network request with `cy.request()`.

- The command `cy.validateSchema()` is chainable and returns the original API response yielded.

- It supports schemas provided as plain **JSON schema**, **OpenAPI 3.0.1 schema document**, and **Swagger 2.0 schema document**, which can be sourced from a Cypress fixture.

- It uses the **Ajv JSON Schema Validator** as its core engine.

- It provides in the **Cypress log** a **summary of the schema errors**, as well as a **list of the individual errors** in the schema validation.

- By clicking on the summary of schema errors in the Cypress log, the console will output:
    - Number of schema errors.
    - Full list of schema errors as provided by Ajv.
    - A **nested tree view of the validated data**, clearly indicating the errors and where they occurred in an **easy-to-understand format**. 

> ⭐⭐⭐⭐⭐
> Note: This plugin would complement Filip Hric's [cypress-plugin-api](https://github.com/filiphric/cypress-plugin-api) and Gleb Bahmutov's [@bahmutov/cy-api](https://github.com/bahmutov/cy-api) plugins to perform JSON schema validations.
>
> Example usage with these two API plugins:
> `cy.api('/users/1').validateSchema(schema)`

&nbsp;  

##### Installation

```sh
npm install cypress-ajv-schema-validator
// or
yarn add cypress-ajv-schema-validator
```

##### Compatibility

- Cypress 12.0.0 or higher
- Ajv 8.16.0 or higher
- ajv-formats 3.0.1 or higher

##### Configuration

Add the following lines either to your `cypress/support/commands.js` to include the custom command and function globally, or directly in the test file that will host the schema validation tests:

- For `cy.validateSchema()` Custom Command:

```js
import 'cypress-ajv-schema-validator'
```

- For `validateSchema()` Function:

```js
import validateSchema from 'cypress-ajv-schema-validator'
```

##### API Reference

###### Custom Command `cy.validateSchema(schema, path)`

Validates the given data against the provided schema.

- Parameters

    - `schema` (object): The schema to validate against. Supported formats are plain JSON schema, Swagger, and OpenAPI documents.
    - `path` (object, optional): The path object to the schema definition in a Swagger or OpenAPI document.
        - `endpoint` (string, optional): The endpoint path.
        - `method` (string, optional): The HTTP method. Defaults to 'GET'.
        - `status` (integer, optional): The response status code. Defaults to 200.

- Returns

    - `Cypress.Chainable`: The response object wrapped in a Cypress.Chainable.

- Throws

    - `Error`: If any of the required parameters are missing or if the schema or schema definition is not found.

Example providing a Plain JSON schema:

```js
cy.request('GET', 'https://awesome.api.com/users/1')
  .validateSchema(schema);
```

Example providing an OpenAPI 3.0.1 or Swagger 2.0 schema documents:

```js
cy.request('GET', 'https://awesome.api.com/users/1')
  .validateSchema(schema, { endpoint: '/users/{id}', method: 'GET', status: 200 });
```


###### Function `validateSchema(data, schema, path)`

Validates the given data against the provided schema.

- Parameters

    - `data` (any): The data to be validated.
    - `schema` (object): The schema to validate against.
    - `path` (object, optional): The path object to the schema definition in a Swagger or OpenAPI document.
        - `endpoint` (string, optional): The endpoint path.
        - `method` (string, optional): The HTTP method. Defaults to 'GET'.
        - `status` (integer, optional): The response status code. Defaults to 200.

- Returns

    - `Array`: An array of validation errors, or null if the data is valid against the schema.

- Throws

    - `Error`: If any of the required parameters are missing or if the schema or schema definition is not found.

Example providing a Plain JSON schema:

```js
cy.request('GET', 'https://awesome.api.com/users/1').then(response => {
  const data = response.body
  const errors = validateSchema(data, schema);
  expect(errors).to.have.length(0); // Assertion to ensure no validation errors
});
```

Example providing an OpenAPI 3.0.1 or Swagger 2.0 schema documents:

```js
cy.request('GET', 'https://awesome.api.com/users/1').then(response => {
  const data = response.body
  const errors = validateSchema(data, schema, { endpoint: '/users/{id}', method: 'GET', status: 200 });
  expect(errors).to.have.length(0); // Assertion to ensure no validation errors
});
```

##### Usage Examples

###### `cy.validateSchema()` command with a **Plain JSON schema**.

```js
describe('API Schema Validation with Plain JSON', () => {
  it('should validate the user data using plain JSON schema', () => {
    const schema = {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "age": { "type": "number" }
      },
      "required": ["name", "age"]
    };

    cy.request('GET', 'https://awesome.api.com/users/1')
      .validateSchema(schema)
      .then(response => {
        // Further assertions
      });
  });
});
```

###### `cy.validateSchema()` command with a **Swagger 2.0 schema** document.

```js
describe('API Schema Validation with Swagger 2.0', () => {
  it('should validate the user data using Swagger 2.0 schema', () => {
    const schema = {
      "swagger": "2.0",
      "paths": {
        "/users/{id}": {
          "get": {
            "responses": {
              "200": {
                "schema": { "$ref": "#/definitions/User" }
              }
            }
          }
        }
      },
      "definitions": {
        "User": {
          "type": "object",
          "properties": {
            "name": { "type": "string" },
            "age": { "type": "number" }
          },
          "required": ["name", "age"]
        }
      }
    };

    const path = { endpoint: '/users/{id}', method: 'GET', status: 200 };

    cy.request('GET', 'https://awesome.api.com/users/1')
      .validateSchema(schema, path)
      .then(response => {
        // Further assertions
      });
  });
});
```

###### `validateSchema()` function with an **OpenAPI 3.0.1 schema** document.

```js
import validateSchema from 'cypress-ajv-schema-validator';

describe('API Schema Validation Function', () => {
  it('should validate the user data using validateSchema function', () => {
    const schema = {
      "openapi": "3.0.1",
      "paths": {
        "/users/{id}": {
          "get": {
            "responses": {
              "200": {
                "content": {
                  "application/json": {
                    "schema": { "$ref": "#/components/schemas/User" }
                  }
                }
              }
            }
          }
        }
      },
      "components": {
        "schemas": {
          "User": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "age": { "type": "number" }
            },
            "required": ["name", "age"]
          }
        }
      }
    };

    const path = { endpoint: '/users/{id}', method: 'GET', status: 200 };

    cy.request('GET', 'https://awesome.api.com/users/1').then(response => {
      const errors = validateSchema(response.body, schema, path);
      
      expect(errors).to.have.length(0); // Assertion to ensure no validation errors
    });
  });
});
```

For more detailed usage examples, check the document [USAGE-EXAMPLES.md](https://github.com/sclavijosuero/cypress-ajv-schema-validator/blob/main/USAGE-EXAMPLES.md) in the plugin GitHub repository.

##### Validation Results

One of the key strengths of the `cypress-ajv-schema-validator` plugin is its **ability to clearly display validation results within the Cypress log**. This feature makes debugging much simpler and more efficient, as it provides immediate, actionable insights right where you need them. Whether your tests pass or fail, the plugin ensures that the feedback is both comprehensive and user-friendly. Let's explore how this works.

###### Test Passed

When a test passes, the Cypress log will show the message: "✔️ PASSED - THE RESPONSE BODY IS VALID AGAINST THE SCHEMA.".

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mn8i1qfyyth8v0ljzlpd.png)

###### Test Failed

When a test fails, the Cypress log will display the message: "❌ FAILED - THE RESPONSE BODY IS NOT VALID AGAINST THE SCHEMA," indicating the total number of errors: (Number of schema errors: N).

Additionally, the Cypress log will provide entries for each individual schema validation error as identified by AJV. Errors related to missing fields in the validated data are marked with the symbol 🗑️, while other types of errors are flagged with the symbol 👉.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f4y06qn34zhs5pt8ohgn.png)

###### Detailed Error View in the Console

For a more in-depth look, open the Console in your browser's DevTools and click on the summary line for the schema validation errors in the Cypress log. The **console will reveal detailed information about all the errors**, including:

- The total number of errors.
- The full list of errors as reported by AJV.
- A **user-friendly view of the validated data**, highlighting the exact location and reason for each validation error.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gtl2xerqvzj081x8s7ju.png)

The plugin continues to provide the list of schema errors in Ajv format, but it also offers the response data that was validated, indicating exactly where the errors are located and precisely what those errors are.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ajl3u70pseog6o3acmno.png)

This content can be copied directly from the console and provided elsewhere in a JSON file.

```json
[
    {
        "id": "👉 \"abc123\" must match format \"uuid\"",
        "description": "👉 null must be string",
        "createdDate": "👉 \"abc123\" must match format \"date-time\"",
        "priority": 3,
        "completed": true,
        "details": {
            "detail1": "👉 789 must be string",
            "detail2": "🗑️ Missing property \"detail2\""
        }
    },
    {
        "id": "👉 null must be string",
        "description": "New Entry",
        "completed": "👉 \"false\" must be boolean",
        "priority": "👉 true must be integer",
        "details": {
            "detail2": 260
        },
        "createdDate": "🗑️ Missing property \"createdDate\""
    }
]
```

By presenting validation results in this clear and structured manner, the `cypress-ajv-schema-validator` plugin not only makes your testing workflow more robust but also saves you valuable time and effort in identifying and resolving issues.

---

### ACT3: RESOLUTION

What an adventure it's been delving into the world of vigilant testing! Just like a vigilante swooping in to restore order in a chaotic city, the **cypress-ajv-schema-validator** plugin ensures your testing environment stays neat, efficient, and just. With its precise JSON schema validation and clear, user-friendly error reporting, this plugin is the hero your API testing workflow deserves.

Imagine AJV as your trusty sidekick, always ready to catch even the most elusive validation errors. And with our plugin, navigating the treacherous streets of API testing becomes a lot less daunting and a lot more exhilarating. Whether you're dealing with JSON schemas, or complete OpenAPI and Swagger documents, this plugin steps in to maintain law and order, ensuring that no violation goes unnoticed.

So don your tester's cape, and add **cypress-ajv-schema-validator** to your arsenal. With this powerful tool, you'll be ready to tackle any challenge that comes your way, maintaining justice and peace in the realm of API testing.

Stay vigilant, stay validated!

**_Don't forget to leave a comment, give a thumbs up, or follow my Cypress blog if you found this new plugin and this post useful. Your feedback and support are always appreciated!_**