From dc24c3d33e73611c3a590288f4b6cff5400c3554 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Mon, 6 Oct 2025 00:24:11 +0300 Subject: [PATCH] replaced joi with zod --- docs/api.md | 26 +++++++++++------ docs/helpers/JSONResponse.md | 28 +++++++++--------- lib/helper/JSONResponse.js | 50 ++++++++++++++------------------ package.json | 2 +- test/helper/JSONResponse_test.js | 29 +++++++++++------- 5 files changed, 73 insertions(+), 62 deletions(-) diff --git a/docs/api.md b/docs/api.md index 80bc146e1..fe037ee54 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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. diff --git a/docs/helpers/JSONResponse.md b/docs/helpers/JSONResponse.md index e9c12ca9c..59399f362 100644 --- a/docs/helpers/JSONResponse.md +++ b/docs/helpers/JSONResponse.md @@ -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 @@ -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 diff --git a/lib/helper/JSONResponse.js b/lib/helper/JSONResponse.js index 08f2be8b9..99c75f6d9 100644 --- a/lib/helper/JSONResponse.js +++ b/lib/helper/JSONResponse.js @@ -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: @@ -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 * @@ -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 @@ -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() { diff --git a/package.json b/package.json index a20833c55..0fb4d9be1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/helper/JSONResponse_test.js b/test/helper/JSONResponse_test.js index b78c1185c..25468983e 100644 --- a/test/helper/JSONResponse_test.js +++ b/test/helper/JSONResponse_test.js @@ -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' @@ -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 = () => { @@ -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') + }) }) })