Repository: cdimascio/express-openapi-validator
Pull request #903 (2024-02-08 23:49:34)

Fixes for 881 - multiple specs w/validateRequests fail
Problem: Multiple api installations cause installation of chained mung'd json implementations each is bound to a specific apiSpec in the surrounding ResponseValidator.  requests are labelled with a specific apiSpec. One (or more) of the chained implementations will then ResponseValidator.validate against a mismatched ResponseValidator.

Solution is:
1. OpenApiSpecLoader.load() decorates its returned Spec with a unique serial number
2. OpenApiContext caches this serial number
3. applyOpenMetadata stores this serial number in the OpenApiRequestMetadata in req.openapi
4. ResponseValidator.constructor caches the serial number when it is created

And, to make it all work,

5. ResponseValidator.validate inside the mung'd json, checks to see if the serial number in req.openapi.serial matches the serial number in the containing ResponseValidator instance.


Patch:
diff --git a/CHANGE_HISTORY.md b/CHANGE_HISTORY.md
index 140523b..34520aa 100644
--- a/CHANGE_HISTORY.md
+++ b/CHANGE_HISTORY.md
@@ -1,7 +1,34 @@
+## <small>5.1.5 (2024-02-10)</small>
+
+* fixes write-only tests ([8c53e58](https://github.com/cdimascio/express-openapi-validator/commit/8c53e58))
+* Support writeOnly + required combination #149 (#756) ([4f16ed2](https://github.com/cdimascio/express-openapi-validator/commit/4f16ed2)), closes [#149](https://github.com/cdimascio/express-openapi-validator/issues/149) [#756](https://github.com/cdimascio/express-openapi-validator/issues/756)
+
+
+
+## <small>5.1.4 (2024-02-09)</small>
+
+* add cookies to examples 1 and 2 (#891) ([2c95d5b](https://github.com/cdimascio/express-openapi-validator/commit/2c95d5b)), closes [#891](https://github.com/cdimascio/express-openapi-validator/issues/891)
+* Direct example broken link to the guide ([00a9c8f](https://github.com/cdimascio/express-openapi-validator/commit/00a9c8f))
+* fixes badging for build and test ([631fb7b](https://github.com/cdimascio/express-openapi-validator/commit/631fb7b))
+* npm audit fix (#892) ([2977c0a](https://github.com/cdimascio/express-openapi-validator/commit/2977c0a)), closes [#892](https://github.com/cdimascio/express-openapi-validator/issues/892)
+* Remove read only and write only fields (#895) ([97617fd](https://github.com/cdimascio/express-openapi-validator/commit/97617fd)), closes [#895](https://github.com/cdimascio/express-openapi-validator/issues/895) [#627](https://github.com/cdimascio/express-openapi-validator/issues/627)
+* removes lodash.uniq and lodash.zipobject dependencies (#893) ([1206802](https://github.com/cdimascio/express-openapi-validator/commit/1206802)), closes [#893](https://github.com/cdimascio/express-openapi-validator/issues/893)
+* Update CONTRIBUTING.md ([6d67169](https://github.com/cdimascio/express-openapi-validator/commit/6d67169))
+* Update README.md ([dffda28](https://github.com/cdimascio/express-openapi-validator/commit/dffda28))
+* Update README.md ([bdd0d79](https://github.com/cdimascio/express-openapi-validator/commit/bdd0d79))
+* Update README.md (#896) ([bb66916](https://github.com/cdimascio/express-openapi-validator/commit/bb66916)), closes [#896](https://github.com/cdimascio/express-openapi-validator/issues/896)
+* v5.1.4 ([b3d7483](https://github.com/cdimascio/express-openapi-validator/commit/b3d7483))
+* v5.1.4 ([509fa22](https://github.com/cdimascio/express-openapi-validator/commit/509fa22))
+* fix: #887 allow multiple params with wildcard (#898) ([2d33d0a](https://github.com/cdimascio/express-openapi-validator/commit/2d33d0a)), closes [#887](https://github.com/cdimascio/express-openapi-validator/issues/887) [#898](https://github.com/cdimascio/express-openapi-validator/issues/898) [#1](https://github.com/cdimascio/express-openapi-validator/issues/1)
+* docs: fix doc typo in README.md (#885) ([8a81bf8](https://github.com/cdimascio/express-openapi-validator/commit/8a81bf8)), closes [#885](https://github.com/cdimascio/express-openapi-validator/issues/885)
+
+
+
 ## <small>5.1.3 (2024-01-27)</small>
 
 * CLS Context is lost after using multer middleware (#695) ([40716fb](https://github.com/cdimascio/express-openapi-validator/commit/40716fb)), closes [#695](https://github.com/cdimascio/express-openapi-validator/issues/695)
 * remove examples from schema (#890) ([0ad49ec](https://github.com/cdimascio/express-openapi-validator/commit/0ad49ec)), closes [#890](https://github.com/cdimascio/express-openapi-validator/issues/890)
+* v5.1.3 ([f806690](https://github.com/cdimascio/express-openapi-validator/commit/f806690))
 * v5.1.3 ([e567701](https://github.com/cdimascio/express-openapi-validator/commit/e567701))
 
 
@@ -117,3 +144,43 @@
 
 
 
+## <small>4.13.8 (2022-05-30)</small>
+
+* Bump AJV to v8 (#713) ([2b27332](https://github.com/cdimascio/express-openapi-validator/commit/2b27332)), closes [#713](https://github.com/cdimascio/express-openapi-validator/issues/713)
+* Bump multer to version that removes dicer as sub-dependency (#739) ([6501a62](https://github.com/cdimascio/express-openapi-validator/commit/6501a62)), closes [#739](https://github.com/cdimascio/express-openapi-validator/issues/739)
+* update ansi-regex ([6448f45](https://github.com/cdimascio/express-openapi-validator/commit/6448f45))
+* Update README.md ([c954b4b](https://github.com/cdimascio/express-openapi-validator/commit/c954b4b))
+* v4.14.0-beta.1 ([369a4a6](https://github.com/cdimascio/express-openapi-validator/commit/369a4a6))
+* v4.14.0-beta.2 ([1706538](https://github.com/cdimascio/express-openapi-validator/commit/1706538))
+* chore(deps): bump ansi-regex in /examples/2-standard-multiple-api-specs (#727) ([3e803b5](https://github.com/cdimascio/express-openapi-validator/commit/3e803b5)), closes [#727](https://github.com/cdimascio/express-openapi-validator/issues/727)
+* chore(deps): bump ansi-regex in /examples/3-eov-operations (#726) ([aba3cd0](https://github.com/cdimascio/express-openapi-validator/commit/aba3cd0)), closes [#726](https://github.com/cdimascio/express-openapi-validator/issues/726)
+* chore(deps): bump ansi-regex in /examples/5-custom-operation-resolver (#725) ([2bf250e](https://github.com/cdimascio/express-openapi-validator/commit/2bf250e)), closes [#725](https://github.com/cdimascio/express-openapi-validator/issues/725)
+* chore(deps): bump ansi-regex in /examples/6-multi-file-spec (#723) ([e29c1eb](https://github.com/cdimascio/express-openapi-validator/commit/e29c1eb)), closes [#723](https://github.com/cdimascio/express-openapi-validator/issues/723)
+* chore(deps): bump ansi-regex in /examples/7-response-date-serialization (#722) ([acdae50](https://github.com/cdimascio/express-openapi-validator/commit/acdae50)), closes [#722](https://github.com/cdimascio/express-openapi-validator/issues/722)
+* chore(deps): bump ansi-regex in /examples/8-top-level-discriminator (#719) ([45059a6](https://github.com/cdimascio/express-openapi-validator/commit/45059a6)), closes [#719](https://github.com/cdimascio/express-openapi-validator/issues/719)
+* chore(deps): bump follow-redirects in /examples/9-nestjs (#705) ([15e91aa](https://github.com/cdimascio/express-openapi-validator/commit/15e91aa)), closes [#705](https://github.com/cdimascio/express-openapi-validator/issues/705)
+* chore(deps): bump minimist from 1.2.5 to 1.2.6 in /examples/1-standard (#714) ([1eecf40](https://github.com/cdimascio/express-openapi-validator/commit/1eecf40)), closes [#714](https://github.com/cdimascio/express-openapi-validator/issues/714)
+* chore(deps): bump minimist in /examples/2-standard-multiple-api-specs (#716) ([6f6ea71](https://github.com/cdimascio/express-openapi-validator/commit/6f6ea71)), closes [#716](https://github.com/cdimascio/express-openapi-validator/issues/716)
+* chore(deps): bump minimist in /examples/3-eov-operations (#715) ([3f6cb37](https://github.com/cdimascio/express-openapi-validator/commit/3f6cb37)), closes [#715](https://github.com/cdimascio/express-openapi-validator/issues/715)
+* chore(deps): bump minimist in /examples/4-eov-operations-babel (#717) ([6b90f35](https://github.com/cdimascio/express-openapi-validator/commit/6b90f35)), closes [#717](https://github.com/cdimascio/express-openapi-validator/issues/717)
+* chore(deps): bump minimist in /examples/5-custom-operation-resolver (#718) ([acc3f33](https://github.com/cdimascio/express-openapi-validator/commit/acc3f33)), closes [#718](https://github.com/cdimascio/express-openapi-validator/issues/718)
+* chore(deps): bump minimist in /examples/6-multi-file-spec (#724) ([15814ba](https://github.com/cdimascio/express-openapi-validator/commit/15814ba)), closes [#724](https://github.com/cdimascio/express-openapi-validator/issues/724)
+* chore(deps): bump minimist in /examples/7-response-date-serialization (#721) ([ff63618](https://github.com/cdimascio/express-openapi-validator/commit/ff63618)), closes [#721](https://github.com/cdimascio/express-openapi-validator/issues/721)
+* chore(deps): bump minimist in /examples/8-top-level-discriminator (#720) ([a23a09f](https://github.com/cdimascio/express-openapi-validator/commit/a23a09f)), closes [#720](https://github.com/cdimascio/express-openapi-validator/issues/720)
+* chore(deps): bump node-fetch from 2.6.1 to 2.6.7 in /examples/9-nestjs (#711) ([87bb6df](https://github.com/cdimascio/express-openapi-validator/commit/87bb6df)), closes [#711](https://github.com/cdimascio/express-openapi-validator/issues/711)
+
+
+
+## <small>4.13.7 (2022-03-27)</small>
+
+* migrate README to wiki ([4887ba5](https://github.com/cdimascio/express-openapi-validator/commit/4887ba5))
+* Update README ([13b26d6](https://github.com/cdimascio/express-openapi-validator/commit/13b26d6))
+
+
+
+## <small>4.13.6 (2022-03-13)</small>
+
+* v4.13.6 ([0deb38f](https://github.com/cdimascio/express-openapi-validator/commit/0deb38f))
+
+
+
diff --git a/examples/2-standard-multiple-api-specs/api.v2.yaml b/examples/2-standard-multiple-api-specs/api.v2.yaml
index b5b7f2e..91a58f8 100644
--- a/examples/2-standard-multiple-api-specs/api.v2.yaml
+++ b/examples/2-standard-multiple-api-specs/api.v2.yaml
@@ -117,9 +117,9 @@ components:
   schemas:
     Pet:
       required:
-        - id
-        - name
-        - type
+        - pet_id
+        - pet_name
+        - pet_type
       properties:
         pet_id:
           readOnly: true
diff --git a/package-lock.json b/package-lock.json
index 6ff155e..b55cb94 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "express-openapi-validator",
-  "version": "5.1.3",
+  "version": "5.1.5",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "express-openapi-validator",
-      "version": "5.1.3",
+      "version": "5.1.5",
       "license": "MIT",
       "dependencies": {
         "@apidevtools/json-schema-ref-parser": "^9.1.2",
diff --git a/package.json b/package.json
index aa09554..d5acdbc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "express-openapi-validator",
-  "version": "5.1.3",
+  "version": "5.1.5",
   "description": "Automatically validate API requests and responses with OpenAPI 3 and Express.",
   "main": "dist/index.js",
   "scripts": {
diff --git a/src/framework/openapi.context.ts b/src/framework/openapi.context.ts
index 2df421f..8d6fc23 100644
--- a/src/framework/openapi.context.ts
+++ b/src/framework/openapi.context.ts
@@ -12,6 +12,7 @@ export class OpenApiContext {
   public readonly routes: RouteMetadata[] = [];
   public readonly ignoreUndocumented: boolean;
   public readonly useRequestUrl: boolean;
+  public readonly serial: number;
   private readonly basePaths: string[];
   private readonly ignorePaths: RegExp | Function;
 
@@ -28,6 +29,7 @@ export class OpenApiContext {
     this.ignoreUndocumented = ignoreUndocumented;
     this.buildRouteMaps(spec.routes);
     this.useRequestUrl = useRequestUrl;
+    this.serial = spec.serial;
   }
 
   public isManagedRoute(path: string): boolean {
diff --git a/src/framework/openapi.spec.loader.ts b/src/framework/openapi.spec.loader.ts
index 44b6a8a..3ccb148 100644
--- a/src/framework/openapi.spec.loader.ts
+++ b/src/framework/openapi.spec.loader.ts
@@ -9,6 +9,7 @@ export interface Spec {
   apiDoc: OpenAPIV3.Document;
   basePaths: string[];
   routes: RouteMetadata[];
+  serial: number;
 }
 
 export interface RouteMetadata {
@@ -23,6 +24,7 @@ interface DiscoveredRoutes {
   apiDoc: OpenAPIV3.Document;
   basePaths: string[];
   routes: RouteMetadata[];
+  serial: number;
 }
 // Sort routes by most specific to least specific i.e. static routes before dynamic
 // e.g. /users/my_route before /users/{id}
@@ -33,6 +35,9 @@ export const sortRoutes = (r1, r2) => {
   return e1 > e2 ? 1 : -1;
 };
 
+// Uniquely identify the Spec that is emitted
+let serial = 0;
+
 export class OpenApiSpecLoader {
   private readonly framework: OpenAPIFramework;
   constructor(opts: OpenAPIFrameworkArgs) {
@@ -91,10 +96,12 @@ export class OpenApiSpecLoader {
 
     routes.sort(sortRoutes);
 
+    serial = serial + 1;
     return {
       apiDoc,
       basePaths,
       routes,
+      serial
     };
   }
 
diff --git a/src/framework/types.ts b/src/framework/types.ts
index eb4f4af..7cc4167 100644
--- a/src/framework/types.ts
+++ b/src/framework/types.ts
@@ -497,6 +497,7 @@ export interface OpenApiRequestMetadata {
   openApiRoute: string;
   pathParams: { [index: string]: string };
   schema: OpenAPIV3.OperationObject;
+  serial: number;
 }
 
 export interface OpenApiRequest extends Request {
diff --git a/src/middlewares/openapi.metadata.ts b/src/middlewares/openapi.metadata.ts
index a137073..12813f0 100644
--- a/src/middlewares/openapi.metadata.ts
+++ b/src/middlewares/openapi.metadata.ts
@@ -48,6 +48,7 @@ export function applyOpenApiMetadata(
         openApiRoute: openApiRoute,
         pathParams: pathParams,
         schema: schema,
+        serial: openApiContext.serial,
       };
       req.params = pathParams;
       if (responseApiDoc) {
@@ -101,6 +102,7 @@ export function applyOpenApiMetadata(
             expressRoute,
             openApiRoute,
             pathParams,
+            serial: -1,
           };
           (<any>r)._responseSchema = _schema;
           return r;
diff --git a/src/middlewares/openapi.response.validator.ts b/src/middlewares/openapi.response.validator.ts
index 6166ca5..a187ad0 100644
--- a/src/middlewares/openapi.response.validator.ts
+++ b/src/middlewares/openapi.response.validator.ts
@@ -32,15 +32,18 @@ export class ResponseValidator {
     [key: string]: { [key: string]: ValidateFunction };
   } = {};
   private eovOptions: ValidateResponseOpts;
+  private serial: number;
 
   constructor(
     openApiSpec: OpenAPIV3.Document,
     options: Options = {},
     eovOptions: ValidateResponseOpts = {},
+    serial: number = -1,
   ) {
     this.spec = openApiSpec;
     this.ajvBody = createResponseAjv(openApiSpec, options);
     this.eovOptions = eovOptions;
+    this.serial = serial;
 
     // This is a pseudo-middleware function. It doesn't get registered with
     // express via `use`
@@ -51,7 +54,7 @@ export class ResponseValidator {
 
   public validate(): RequestHandler {
     return mung.json((body, req, res) => {
-      if (req.openapi) {
+      if (req.openapi && this.serial == req.openapi.serial) {
         const openapi = <OpenApiRequestMetadata>req.openapi;
         // instead of openapi.schema, use openapi._responseSchema to get the response copy
         const responses: OpenAPIV3.ResponsesObject = (<any>openapi)
diff --git a/src/middlewares/parsers/schema.preprocessor.ts b/src/middlewares/parsers/schema.preprocessor.ts
index 8d9d894..8e14f4c 100644
--- a/src/middlewares/parsers/schema.preprocessor.ts
+++ b/src/middlewares/parsers/schema.preprocessor.ts
@@ -287,6 +287,7 @@ export class SchemaPreprocessor {
         // This null check should no longer be necessary
         this.handleSerDes(pschema, nschema, options);
         this.handleReadonly(pschema, nschema, options);
+        this.handleWriteonly(pschema, nschema, options);
         this.processDiscriminator(pschema, nschema, options);
         this.removeExamples(pschema, nschema, options)
       }
@@ -479,6 +480,27 @@ export class SchemaPreprocessor {
     }
   }
 
+  private handleWriteonly(
+    parent: OpenAPIV3.SchemaObject,
+    schema: OpenAPIV3.SchemaObject,
+    opts,
+  ) {
+    if (opts.kind === 'req') return;
+
+    const required = parent?.required ?? [];
+    const prop = opts?.path?.[opts?.path?.length - 1];
+    const index = required.indexOf(prop);
+    if (schema.writeOnly && index > -1) {
+      // remove required if writeOnly
+      parent.required = required
+        .slice(0, index)
+        .concat(required.slice(index + 1));
+      if (parent.required.length === 0) {
+        delete parent.required;
+      }
+    }
+  }
+
   /**
    * extract all requestBodies' schemas from an operation
    * @param op
diff --git a/src/openapi.validator.ts b/src/openapi.validator.ts
index c4b639e..43073be 100644
--- a/src/openapi.validator.ts
+++ b/src/openapi.validator.ts
@@ -35,6 +35,12 @@ export {
   Forbidden,
 } from './framework/types';
 
+interface MiddlewareContext {
+  context: OpenApiContext,
+  responseApiDoc: OpenAPIV3.Document,
+  error: any,
+}
+
 export class OpenApiValidator {
   readonly options: NormalizedOpenApiValidatorOpts;
   readonly ajvOpts: AjvOptions;
@@ -92,7 +98,7 @@ export class OpenApiValidator {
 
   installMiddleware(spec: Promise<Spec>): OpenApiRequestHandler[] {
     const middlewares: OpenApiRequestHandler[] = [];
-    const pContext = spec
+    const pContext: Promise<MiddlewareContext> = spec
       .then((spec) => {
         const apiDoc = spec.apiDoc;
         const ajvOpts = this.ajvOpts.preprocessor;
@@ -183,7 +189,7 @@ export class OpenApiValidator {
       });
     }
 
-    // request middlweare
+    // request middleware
     if (this.options.validateRequests) {
       let reqmw;
       middlewares.push(function requestMiddleware(req, res, next) {
@@ -201,8 +207,8 @@ export class OpenApiValidator {
       let resmw;
       middlewares.push(function responseMiddleware(req, res, next) {
         return pContext
-          .then(({ responseApiDoc }) => {
-            resmw = resmw || self.responseValidationMiddleware(responseApiDoc);
+          .then(({ responseApiDoc, context: { serial } }) => {
+            resmw = resmw || self.responseValidationMiddleware(responseApiDoc, serial);
             return resmw(req, res, next);
           })
           .catch(next);
@@ -288,12 +294,13 @@ export class OpenApiValidator {
     return (req, res, next) => requestValidator.validate(req, res, next);
   }
 
-  private responseValidationMiddleware(apiDoc: OpenAPIV3.Document) {
+  private responseValidationMiddleware(apiDoc: OpenAPIV3.Document, serial: number) {
     return new middlewares.ResponseValidator(
       apiDoc,
       this.ajvOpts.response,
       // This has already been converted from boolean if required
       this.options.validateResponses as ValidateResponseOpts,
+      serial
     ).validate();
   }
 


Test patch:
diff --git a/test/356.campaign.spec.ts b/test/356.campaign.spec.ts
index 57004cd..fbe834d 100644
--- a/test/356.campaign.spec.ts
+++ b/test/356.campaign.spec.ts
@@ -1,4 +1,4 @@
-import * as path from 'path';
+ import * as path from 'path';
 import * as express from 'express';
 import * as request from 'supertest';
 import { createApp } from './common/app';
diff --git a/test/881.spec.ts b/test/881.spec.ts
new file mode 100644
index 0000000..33bd3d1
--- /dev/null
+++ b/test/881.spec.ts
@@ -0,0 +1,119 @@
+import { expect } from 'chai';
+import * as request from 'supertest';
+
+describe('multi-spec', () => {
+  let app = null;
+
+  before(async () => {
+    // Set up the express app
+    app = createServer();
+  });
+
+  after(() => {
+    app.server.close();
+  });
+
+  it('create campaign should return 200', async () =>
+    request(app)
+      .get(`/v1/pets`)
+      .expect(400)
+      .then((r) => {
+        expect(r.body.message).include('limit');
+        expect(r.body.message).include(`'type'`);
+      }));
+
+  it('create campaign should return 200', async () =>
+    request(app)
+      .get(`/v2/pets`)
+      .expect(400)
+      .then((r) => {
+        expect(r.body.message).include(`'pet_type'`);
+      }));
+});
+
+function createServer() {
+  const express = require('express');
+  const path = require('path');
+  const bodyParser = require('body-parser');
+  const http = require('http');
+  const OpenApiValidator = require('../src');
+
+  const app = express();
+  app.use(bodyParser.urlencoded({ extended: false }));
+  app.use(bodyParser.text());
+  app.use(bodyParser.json());
+
+  const versions = [1, 2];
+
+  for (const v of versions) {
+    const apiSpec = path.join(__dirname, `api.v${v}.yaml`);
+    app.use(
+      OpenApiValidator.middleware({
+        apiSpec,
+        validateResponses: true,
+      }),
+    );
+
+    routes(app, v);
+  }
+
+  const server = http.createServer(app);
+  server.listen(3000);
+  console.log('Listening on port 3000');
+
+  function routes(app, v) {
+    if (v === 1) routesV1(app);
+    if (v === 2) routesV2(app);
+  }
+
+  function routesV1(app) {
+    const v = '/v1';
+    app.post(`${v}/pets`, (req, res, next) => {
+      res.json({ ...req.body });
+    });
+    app.get(`${v}/pets`, (req, res, next) => {
+      res.json([
+        {
+          id: 1,
+          name: 'happy',
+          type: 'cat',
+        },
+      ]);
+    });
+
+    app.use((err, req, res, next) => {
+      // format error
+      res.status(err.status || 500).json({
+        message: err.message,
+        errors: err.errors,
+      });
+    });
+  }
+
+  function routesV2(app) {
+    const v = '/v2';
+    app.get(`${v}/pets`, (req, res, next) => {
+      res.json([
+        {
+          pet_id: 1,
+          pet_name: 'happy',
+          pet_type: 'kitty',
+        },
+      ]);
+    });
+    app.post(`${v}/pets`, (req, res, next) => {
+      res.json({ ...req.body });
+    });
+
+    app.use((err, req, res, next) => {
+      // format error
+      res.status(err.status || 500).json({
+        message: err.message,
+        errors: err.errors,
+      });
+    });
+  }
+
+  app.server = server;
+  return app;
+}
diff --git a/test/api.v2.yaml b/test/api.v2.yaml
index b5b7f2e..91a58f8 100644
--- a/test/api.v2.yaml
+++ b/test/api.v2.yaml
@@ -117,9 +117,9 @@ components:
   schemas:
     Pet:
       required:
-        - id
-        - name
-        - type
+        - pet_id
+        - pet_name
+        - pet_type
       properties:
         pet_id:
           readOnly: true
diff --git a/test/resources/write.only.yaml b/test/resources/write.only.yaml
index 64db4be..cf8d62e 100644
--- a/test/resources/write.only.yaml
+++ b/test/resources/write.only.yaml
@@ -95,6 +95,8 @@ components:
     # TODO add nested test
     ProductNested:
       type: object
+      required:
+        - "password"
       properties:
         id:
           type: string
@@ -117,9 +119,14 @@ components:
           type: array
           items:
             $ref: '#/components/schemas/Review'
+        password:
+          type: string
+          writeOnly: true
 
     Review:
       type: object
+      required:
+        - "review_password"
       properties:
         id:
           type: integer
@@ -132,3 +139,6 @@ components:
           writeOnly: true
         rating:
           type: integer
+        review_password:
+          type: string
+          writeOnly: true
diff --git a/test/write.only.removeadditional.spec.ts b/test/write.only.removeadditional.spec.ts
index e50da1d..cbf9161 100644
--- a/test/write.only.removeadditional.spec.ts
+++ b/test/write.only.removeadditional.spec.ts
@@ -10,31 +10,34 @@ describe(packageJson.name, () => {
   before(async () => {
     // Set up the express app
     const apiSpec = path.join('test', 'resources', 'write.only.yaml');
-    app = await createApp({ apiSpec, validateResponses: {removeAdditional : true} }, 3005, app =>
-      app
-        .post(`${app.basePath}/products/inlined`, (req, res) => {
-          const body = req.body;
-          const excludeWriteOnly = req.query.exclude_write_only;
-          if (excludeWriteOnly) {
-            delete body.role;
-          }
-          res.json(body);
-        })
-        .post(`${app.basePath}/products/nested`, (req, res) => {
-          const body = req.body;
-          const excludeWriteOnly = req.query.exclude_write_only;
-          body.id = 'test';
-          body.created_at = new Date().toISOString();
-          body.reviews = body.reviews.map(r => ({
-            ...(excludeWriteOnly ? {} : { role_x: 'admin' }),
-            rating: r.rating ?? 2,
-          }));
+    app = await createApp(
+      { apiSpec, validateResponses: { removeAdditional: true } },
+      3005,
+      (app) =>
+        app
+          .post(`${app.basePath}/products/inlined`, (req, res) => {
+            const body = req.body;
+            const excludeWriteOnly = req.query.exclude_write_only;
+            if (excludeWriteOnly) {
+              delete body.role;
+            }
+            res.json(body);
+          })
+          .post(`${app.basePath}/products/nested`, (req, res) => {
+            const body = req.body;
+            const excludeWriteOnly = req.query.exclude_write_only;
+            body.id = 'test';
+            body.created_at = new Date().toISOString();
+            body.reviews = body.reviews.map((r) => ({
+              ...(excludeWriteOnly ? {} : { role_x: 'admin' }),
+              rating: r.rating ?? 2,
+            }));
 
-          if (excludeWriteOnly) {
-            delete body.role;
-          }
-          res.json(body);
-        }),
+            if (excludeWriteOnly) {
+              delete body.role;
+            }
+            res.json(body);
+          }),
     );
   });
 
@@ -52,7 +55,7 @@ describe(packageJson.name, () => {
         price: 10.99,
       })
       .expect(200)
-      .then(r => {
+      .then((r) => {
         const body = r.body;
         expect(body.message).to.be.undefined;
         expect(body.role).to.be.undefined;
@@ -71,8 +74,7 @@ describe(packageJson.name, () => {
         role: 'admin',
         price: 10.99,
       })
-      .expect(200)
-  );
+      .expect(200));
 
   it('should remove write only properties in responses (nested schema $refs) thanks to removeAdditional', async () =>
     request(app)
@@ -81,18 +83,18 @@ describe(packageJson.name, () => {
       .send({
         name: 'some name',
         price: 10.99,
+        password: 'password_value',
         reviews: [
           {
-            rating: 5
+            rating: 5,
+            review_password: 'review_password_value',
           },
         ],
       })
       .expect(200)
-      .then(r => {
+      .then((r) => {
         const body = r.body;
-        console.log('%j', r.body);
         expect(body.reviews[0].role_x).to.be.undefined;
         expect(body.reviews[0].rating).to.be.equal(5);
       }));
-
 });
diff --git a/test/write.only.spec.ts b/test/write.only.spec.ts
index 04b6d4b..565fcbf 100644
--- a/test/write.only.spec.ts
+++ b/test/write.only.spec.ts
@@ -10,7 +10,7 @@ describe(packageJson.name, () => {
   before(async () => {
     // Set up the express app
     const apiSpec = path.join('test', 'resources', 'write.only.yaml');
-    app = await createApp({ apiSpec, validateResponses: true }, 3005, app =>
+    app = await createApp({ apiSpec, validateResponses: true }, 3005, (app) =>
       app
         .post(`${app.basePath}/products/inlined`, (req, res) => {
           const body = req.body;
@@ -18,22 +18,29 @@ describe(packageJson.name, () => {
           if (excludeWriteOnly) {
             delete body.role;
           }
-          res.json(body);
+          res.json({
+            ...body,
+          });
         })
         .post(`${app.basePath}/products/nested`, (req, res) => {
           const body = req.body;
           const excludeWriteOnly = req.query.exclude_write_only;
           body.id = 'test';
           body.created_at = new Date().toISOString();
-          body.reviews = body.reviews.map(r => ({
+          body.reviews = body.reviews.map((r) => ({
             ...(excludeWriteOnly ? {} : { role_x: 'admin' }),
             rating: r.rating ?? 2,
           }));
 
           if (excludeWriteOnly) {
             delete body.role;
+            delete body.password;
           }
-          res.json(body);
+          res.json({
+            // id: 'xxxxx',
+            // created_at: '2024-02-09T17:32:28Z',
+            ...body,
+          });
         }),
     );
   });
@@ -52,7 +59,7 @@ describe(packageJson.name, () => {
         created_at: new Date().toUTCString(),
       })
       .expect(400)
-      .then(r => {
+      .then((r) => {
         const body = r.body;
         // id is a readonly property and should not be allowed in the request
         expect(body.message).to.contain('created_at');
@@ -68,7 +75,7 @@ describe(packageJson.name, () => {
         price: 10.99,
       })
       .expect(500)
-      .then(r => {
+      .then((r) => {
         const body = r.body;
         expect(body.message).to.contain('role');
         expect(body.errors[0].path).to.contain('/response/role');
@@ -86,9 +93,35 @@ describe(packageJson.name, () => {
         name: 'some name',
         role: 'admin',
         price: 10.99,
+        password: 'password_value',
       })
       .expect(200));
 
+  it('should return 200 if no write-only properties are in the responses', async () =>
+    request(app)
+      .post(`${app.basePath}/products/nested`)
+      .query({
+        exclude_write_only: true,
+      })
+      .set('content-type', 'application/json')
+      .send({
+        name: 'some name',
+        price: 10.99,
+        password: 'password_value',
+        reviews: [
+          {
+            rating: 5,
+            review_password: 'review_password_value',
+          },
+        ],
+      })
+      .expect(200)
+      .then((r) => {
+        const body = r.body;
+        // check that read-only props were not affected and present in the response
+        expect(body.created_at).to.be;
+      }));
+
   it('should not allow write only properties in responses (nested schema $refs)', async () =>
     request(app)
       .post(`${app.basePath}/products/nested`)
@@ -96,14 +129,16 @@ describe(packageJson.name, () => {
       .send({
         name: 'some name',
         price: 10.99,
+        password: 'password_value',
         reviews: [
           {
             rating: 5,
+            review_password: 'review_password_value',
           },
         ],
       })
       .expect(500)
-      .then(r => {
+      .then((r) => {
         const body = r.body;
         expect(body.message).to.contain('role_x');
         expect(body.errors[0].path).to.contain('/response/reviews/0/role_x');
@@ -129,7 +164,7 @@ describe(packageJson.name, () => {
         ],
       })
       .expect(400)
-      .then(r => {
+      .then((r) => {
         const body = r.body;
         expect(body.message).to.contain('request/body/reviews/0/id');
       }));