From f06a2d25ceb37fe6ece1fdfc6733a4b80e67d180 Mon Sep 17 00:00:00 2001 From: Carmine DiMascio Date: Sun, 8 Nov 2020 20:51:25 -0500 Subject: [PATCH] feat: option to enable response body casting (#451) --- README.md | 43 ++++++++------- src/framework/types.ts | 1 + src/middlewares/openapi.request.validator.ts | 8 +-- src/middlewares/openapi.response.validator.ts | 5 +- src/openapi.validator.ts | 16 +++++- test/response.validation.coerce.types.spec.ts | 54 +++++++++++++++++++ 6 files changed, 97 insertions(+), 30 deletions(-) create mode 100644 test/response.validation.coerce.types.spec.ts diff --git a/README.md b/README.md index 571a0869..0803af5f 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ [![GitHub stars](https://img.shields.io/github/stars/cdimascio/express-openapi-validator.svg?style=social&label=Star&maxAge=2592000)](https://GitHub.com/cdimascio/express-openapi-validator/stargazers/) [![Twitter URL](https://img.shields.io/twitter/url/https/github.com/cdimascio/express-openapi-validator.svg?style=social)](https://twitter.com/intent/tweet?text=Check%20out%20express-openapi-validator%20by%20%40CarmineDiMascio%20https%3A%2F%2Fgithub.com%2Fcdimascio%2Fexpress-openapi-validator%20%F0%9F%91%8D) -## Install +## Install ```shell npm install express-openapi-validator @@ -66,7 +66,6 @@ _**Important:** Ensure express is configured with all relevant body parsers. Bod In v4.x.x, the validator is installed as standard connect middleware using `app.use(...) and/or router.use(...)` ([example](https://github.com/cdimascio/express-openapi-validator/blob/v4/README.md#Example-Express-API-Server)). This differs from the v3.x.x the installation which required the `install` method(s). The `install` methods no longer exist in v4. - ## Usage (options) See [Advanced Usage](#Advanced-Usage) options to: @@ -107,12 +106,11 @@ app.use('/spec', express.static(spec)); // 4. Install the OpenApiValidator onto your express app app.use( -OpenApiValidator.middleware({ - apiSpec: './api.yaml', - validateResponses: true, // <-- to validate responses - // unknownFormats: ['my-format'] // <-- to provide custom formats - } - ), + OpenApiValidator.middleware({ + apiSpec: './api.yaml', + validateResponses: true, // <-- to validate responses + // unknownFormats: ['my-format'] // <-- to provide custom formats + }), ); // 5. Define routes using Express @@ -557,6 +555,12 @@ Determines whether the validator should validate responses. Also accepts respons - `"failing"` - additional properties that fail schema validation are automatically removed from the response. + **coerceTypes:** + + - `true` - coerce scalar data types. + - `false` - (**default**) do not coerce types. (almost always the desired behavior) + - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). + For example: ```javascript @@ -593,9 +597,9 @@ Determines whether the validator should validate securities e.g. apikey, basic, Defines a list of custome formats. - `[{ ... }]` - array of custom format objects. Each object must have the following properties: - - name: string (required) - the format name - - validate: (v: any) => boolean (required) - the validation function - - type: 'string' | 'number' (optional) - the format's type + - name: string (required) - the format name + - validate: (v: any) => boolean (required) - the validation function + - type: 'string' | 'number' (optional) - the format's type e.g. @@ -605,23 +609,23 @@ formats: [ name: 'my-three-digit-format', type: 'number', // validate returns true the number has 3 digits, false otherwise - validate: v => /^\d{3}$/.test(v.toString()) + validate: (v) => /^\d{3}$/.test(v.toString()), }, { name: 'my-three-letter-format', type: 'string', // validate returns true the string has 3 letters, false otherwise - validate: v => /^[A-Za-z]{3}$/.test(v) + validate: (v) => /^[A-Za-z]{3}$/.test(v), }, -] +]; ``` Then use it in a spec e.g. ```yaml - my_property: - type: string - format: my-three-letter-format' +my_property: + type: string + format: my-three-letter-format' ``` ### ▪️ validateFormats (optional) @@ -758,11 +762,9 @@ $refParser: { Determines whether the validator should coerce value types to match the those defined in the OpenAPI spec. This option applies **only** to path params, query strings, headers, and cookies. _It is **highly unlikley** that will want to disable this. As such this option is deprecated and will be removed in the next major version_ - - `true` (**default**) - coerce scalar data types. - `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). - ## The Base URL The validator will only validate requests, securities, and responses that are under @@ -1040,7 +1042,7 @@ app.use(OpenApiValidator.middleware({ **A:** First, it's important to note that this behavior does not impact validation. The validator will validate against the type defined in your spec. -In order to modify the `req.params`, express requires that a param handler be registered e.g. `app.param(...)` or `router.param(...)`. Since `app` is available to middleware functions, the validator registers an `app.param` handler to coerce and modify the values of `req.params` to their declared types. Unfortunately, express does not provide a means to determine the current router from a middleware function, hence the validator is unable to register the same param handler on an express router. Ultimately, this means if your handler function is defined on `app`, the values of `req.params` will be coerced to their declared types. If your handler function is declare on an `express.Router`, the values of `req.params` values will be of type `string` (You must coerce them e.g. `parseInt(req.params.id)`). +In order to modify the `req.params`, express requires that a param handler be registered e.g. `app.param(...)` or `router.param(...)`. Since `app` is available to middleware functions, the validator registers an `app.param` handler to coerce and modify the values of `req.params` to their declared types. Unfortunately, express does not provide a means to determine the current router from a middleware function, hence the validator is unable to register the same param handler on an express router. Ultimately, this means if your handler function is defined on `app`, the values of `req.params` will be coerced to their declared types. If your handler function is declare on an `express.Router`, the values of `req.params` values will be of type `string` (You must coerce them e.g. `parseInt(req.params.id)`). ## Contributors ✨ @@ -1110,6 +1112,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d + This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/src/framework/types.ts b/src/framework/types.ts index 208bb6d1..b8740f9b 100644 --- a/src/framework/types.ts +++ b/src/framework/types.ts @@ -46,6 +46,7 @@ export type ValidateRequestOpts = { export type ValidateResponseOpts = { removeAdditional?: 'failing' | boolean; + coerceTypes?: boolean | 'array'; }; export type ValidateSecurityOpts = { diff --git a/src/middlewares/openapi.request.validator.ts b/src/middlewares/openapi.request.validator.ts index 53de7493..d56ceeea 100644 --- a/src/middlewares/openapi.request.validator.ts +++ b/src/middlewares/openapi.request.validator.ts @@ -45,7 +45,7 @@ export class RequestValidator { this.apiDoc = apiDoc; this.requestOpts.allowUnknownQueryParameters = options.allowUnknownQueryParameters; - this.ajv = createRequestAjv(apiDoc, options); + this.ajv = createRequestAjv(apiDoc, { ...options, coerceTypes: true }); this.ajvBody = createRequestAjv(apiDoc, { ...options, coerceTypes: false }); } @@ -141,12 +141,12 @@ export class RequestValidator { : undefined; const data = { - query: req.query ?? {}, + query: req.query ?? {}, headers: req.headers, - params: req.params, + params: req.params, cookies, body: req.body, - } + }; const valid = validator.validatorGeneral(data); const validBody = validator.validatorBody(data); diff --git a/src/middlewares/openapi.response.validator.ts b/src/middlewares/openapi.response.validator.ts index 8f7fdb97..55569988 100644 --- a/src/middlewares/openapi.response.validator.ts +++ b/src/middlewares/openapi.response.validator.ts @@ -33,10 +33,7 @@ export class ResponseValidator { constructor(openApiSpec: OpenAPIV3.Document, options: ajv.Options = {}) { this.spec = openApiSpec; - this.ajvBody = createResponseAjv(openApiSpec, { - ...options, - coerceTypes: false, - }); + this.ajvBody = createResponseAjv(openApiSpec, options); (mung).onError = (err, req, res, next) => { return next(err); diff --git a/src/openapi.validator.ts b/src/openapi.validator.ts index eb286c80..e95013dc 100644 --- a/src/openapi.validator.ts +++ b/src/openapi.validator.ts @@ -43,7 +43,6 @@ export class OpenApiValidator { this.validateOptions(options); this.normalizeOptions(options); - if (options.coerceTypes == null) options.coerceTypes = true; if (options.validateRequests == null) options.validateRequests = true; if (options.validateResponses == null) options.validateResponses = false; if (options.validateSecurity == null) options.validateSecurity = true; @@ -71,6 +70,7 @@ export class OpenApiValidator { if (options.validateResponses === true) { options.validateResponses = { removeAdditional: false, + coerceTypes: false, }; } @@ -313,7 +313,18 @@ export class OpenApiValidator { 'securityHandlers is not supported. Use validateSecurities.handlers instead.', ); } + + const coerceResponseTypes = options?.validateResponses?.['coerceTypes']; + if (options.coerceTypes != null && coerceResponseTypes != null) { + throw ono( + 'coerceTypes and validateResponses.coerceTypes are mutually exclusive', + ); + } + if (options.coerceTypes) { + if (options?.validateResponses) { + options.validateResponses['coerceTypes'] = true; + } console.warn('coerceTypes is deprecated.'); } @@ -364,12 +375,13 @@ class AjvOptions { } get response(): ajv.Options { - const { removeAdditional } = ( + const { coerceTypes, removeAdditional } = ( this.options.validateResponses ); return { ...this.baseOptions(), useDefaults: false, + coerceTypes, removeAdditional, }; } diff --git a/test/response.validation.coerce.types.spec.ts b/test/response.validation.coerce.types.spec.ts new file mode 100644 index 00000000..f13108a1 --- /dev/null +++ b/test/response.validation.coerce.types.spec.ts @@ -0,0 +1,54 @@ +import * as path from 'path'; +import { expect } from 'chai'; +import * as request from 'supertest'; +import { createApp } from './common/app'; + +const apiSpecPath = path.join('test', 'resources', 'response.validation.yaml'); + +describe('response validation with type coercion', () => { + let app = null; + + before(async () => { + // set up express app + app = await createApp( + { + apiSpec: apiSpecPath, + validateResponses: { + coerceTypes: true, + }, + }, + 3005, + (app) => { + app + .get(`${app.basePath}/boolean`, (req, res) => { + return res.json(req.query.value); + }) + .get(`${app.basePath}/object`, (req, res) => { + return res.json({ + id: '1', // we expect this to type coerce to number + name: 'name', + tag: 'tag', + bought_at: null, + }); + }); + }, + false, + ); + }); + + after(() => { + app.server.close(); + }); + + it('should be able to return `true` as the response body', async () => + request(app) + .get(`${app.basePath}/boolean?value=true`) + .expect(200) + .then((r: any) => { + expect(r.body).to.equal(true); + })); + it('should coerce id from string to number', async () => + request(app) + .get(`${app.basePath}/object`) + .expect(200)); +});