Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,25 +267,33 @@ I.seeResponseContainsKeys(['name', 'email']);
> ℹ️ If response is an array, it will check that every element in array have provided keys

However, this is a very naive approach. It won't work for arrays or nested objects.
To check complex JSON structures `JSONResponse` helper uses [`joi`](https://joi.dev) library.
It has rich API to validate JSON by the schema defined using JavaScript.
To check complex JSON structures `JSONResponse` helper uses [`Zod`](https://zod.dev) library.
It has rich API to validate JSON by the schema defined using JavaScript.

```js
// require joi library,
// import zod library,
// it is installed with CodeceptJS
const Joi = require('joi');
import { z } from 'zod';

// create schema definition using Joi API
const schema = Joi.object().keys({
email: Joi.string().email().required(),
phone: Joi.string().regex(/^\d{3}-\d{3}-\d{4}$/).required(),
birthday: Joi.date().max('1-1-2004').iso()
// create schema definition using Zod API
const schema = z.object({
email: z.string().email(),
phone: z.string().regex(/^\d{3}-\d{3}-\d{4}$/),
birthday: z.string().datetime().max(new Date('2004-01-01'))
});

// check that response matches that schema
I.seeResponseMatchesJsonSchema(schema);
```

> 📋 **Migration Note**: CodeceptJS has migrated from Joi to Zod v4 for JSON schema validation.
> If you have existing tests using Joi, please update them:
> * Replace `const Joi = require('joi')` with `import { z } from 'zod'`
> * Replace `Joi.object().keys({...})` with `z.object({...})`
> * Replace `Joi.string().email()` with `z.string().email()`
> * Replace `Joi.date()` with appropriate `z.string()` or `z.date()` types
> * See [Zod documentation](https://zod.dev) for complete API reference

### Data Inclusion

To check that response contains expected data use `I.seeResponseContainsJson` method.
Expand Down
28 changes: 14 additions & 14 deletions docs/helpers/JSONResponse.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,28 +197,28 @@ I.seeResponseEquals({ error: 'Not allowed' })

### seeResponseMatchesJsonSchema

Validates JSON structure of response using [joi library][4].
See [joi API][5] for complete reference on usage.
Validates JSON structure of response using [Zod library][4].
See [Zod API][5] for complete reference on usage.

Use pre-initialized joi instance by passing function callback:
Use pre-initialized Zod instance by passing function callback:

```js
// response.data is { name: 'jon', id: 1 }

I.seeResponseMatchesJsonSchema(joi => {
return joi.object({
name: joi.string(),
id: joi.number()
I.seeResponseMatchesJsonSchema(z => {
return z.object({
name: z.string(),
id: z.number()
})
});

// or pass a valid schema
const joi = require('joi');
import { z } from 'zod';

I.seeResponseMatchesJsonSchema(joi.object({
name: joi.string(),
id: joi.number();
});
I.seeResponseMatchesJsonSchema(z.object({
name: z.string(),
id: z.number()
}));
```

#### Parameters
Expand Down Expand Up @@ -248,8 +248,8 @@ I.seeResponseValidByCallback(({ data, status }) => {

[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array

[4]: https://joi.dev
[4]: https://zod.dev

[5]: https://joi.dev/api/
[5]: https://zod.dev/

[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
50 changes: 22 additions & 28 deletions lib/helper/JSONResponse.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Helper from '@codeceptjs/helper'
import assert from 'assert'
import joi from 'joi'
import { z } from 'zod'

/**
* This helper allows performing assertions on JSON responses paired with following helpers:
Expand Down Expand Up @@ -87,16 +87,7 @@ class JSONResponse extends Helper {
this.response = null
}

static _checkRequirements() {
try {
// In ESM, joi is already imported at the top
// The import will fail at module load time if joi is missing
return null
} catch (e) {
return ['joi']
}
}


/**
* Checks that response code is equal to the provided one
*
Expand Down Expand Up @@ -308,28 +299,28 @@ class JSONResponse extends Helper {
}

/**
* Validates JSON structure of response using [joi library](https://joi.dev).
* See [joi API](https://joi.dev/api/) for complete reference on usage.
* Validates JSON structure of response using [Zod library](https://zod.dev).
* See [Zod API](https://zod.dev/) for complete reference on usage.
*
* Use pre-initialized joi instance by passing function callback:
* Use pre-initialized Zod instance by passing function callback:
*
* ```js
* // response.data is { name: 'jon', id: 1 }
*
* I.seeResponseMatchesJsonSchema(joi => {
* return joi.object({
* name: joi.string(),
* id: joi.number()
* I.seeResponseMatchesJsonSchema(z => {
* return z.object({
* name: z.string(),
* id: z.number()
* })
* });
*
* // or pass a valid schema
* const joi = require('joi');
* import { z } from 'zod';
*
* I.seeResponseMatchesJsonSchema(joi.object({
* name: joi.string(),
* id: joi.number();
* });
* I.seeResponseMatchesJsonSchema(z.object({
* name: z.string(),
* id: z.number()
* }));
* ```
*
* @param {any} fnOrSchema
Expand All @@ -338,14 +329,17 @@ class JSONResponse extends Helper {
this._checkResponseReady()
let schema = fnOrSchema
if (typeof fnOrSchema === 'function') {
schema = fnOrSchema(joi)
schema = fnOrSchema(z)
const body = fnOrSchema.toString()
fnOrSchema.toString = () => `${body.split('\n')[1]}...`
}
if (!schema) throw new Error('Empty Joi schema provided, see https://joi.dev/ for details')
if (!joi.isSchema(schema)) throw new Error('Invalid Joi schema provided, see https://joi.dev/ for details')
schema.toString = () => schema.describe()
joi.assert(this.response.data, schema)
if (!schema) throw new Error('Empty Zod schema provided, see https://zod.dev/ for details')
if (!(schema instanceof z.ZodType)) throw new Error('Invalid Zod schema provided, see https://zod.dev/ for details')
schema.toString = () => schema._def.description || JSON.stringify(schema._def)
const result = schema.parse(this.response.data)
if (!result) {
throw new Error('Schema validation failed')
}
}

_checkResponseReady() {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"html-minifier-terser": "7.2.0",
"inquirer": "12.9.2",
"invisi-data": "^1.1.2",
"joi": "17.13.3",
"zod": "^4.0.0",
"js-beautify": "1.15.4",
"lodash.clonedeep": "4.5.0",
"lodash.merge": "4.6.2",
Expand Down
29 changes: 19 additions & 10 deletions test/helper/JSONResponse_test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import chai from 'chai'
import joi from 'joi'
import { z } from 'zod'
import { JSONResponse } from '../../lib/helper/JSONResponse.js'
import Container from '../../lib/container.js'
import * as codeceptjs from '../../lib/index.js'
Expand Down Expand Up @@ -149,16 +149,16 @@ describe('JSONResponse', () => {
expect(fn.toString()).to.include('expect(data).to.have')
})

it('should check for json by joi schema', () => {
it('should check for json by zod schema', () => {
restHelper.config.onResponse({ data })
const schema = joi.object({
posts: joi.array().items({
id: joi.number(),
author: joi.string(),
title: joi.string(),
}),
user: joi.object({
name: joi.string(),
const schema = z.object({
posts: z.array(z.object({
id: z.number(),
author: z.string(),
title: z.string(),
})),
user: z.object({
name: z.string(),
}),
})
const fn = () => {
Expand All @@ -167,5 +167,14 @@ describe('JSONResponse', () => {
I.seeResponseMatchesJsonSchema(fn)
I.seeResponseMatchesJsonSchema(schema)
})

it('should throw error when zod validation fails', () => {
restHelper.config.onResponse({ data: { name: 'invalid', age: 'not_a_number' } })
const schema = z.object({
name: z.string(),
age: z.number(),
})
expect(() => I.seeResponseMatchesJsonSchema(schema)).to.throw('Schema validation failed')
})
})
})