Repository: nestjs/swagger (ts)

Issue:
Better support for raw component / schemas definitions
<!--
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.

ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
-->

## I'm submitting a...
<!-- 
Please search GitHub for a similar issue or PR before submitting.
Check one of the following options with "x" -->
<pre><code>
[ ] Regression <!--(a behavior that used to work and stopped working in a new release)-->
[ ] Bug report
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.
</code></pre>

## Current behavior
<!-- Describe how the issue manifests. -->

I have a complex model, basically a top-level oneOf, which is not supported by @nestjs/swagger:

```ts

/**
 * PersonGet is supposed to be CompanyGet | UserGet
 *
 * Nestjs swagger does not support a oneOf at the top level of a model. As such we create an OpenAPI definition manually
 */

type SchemaObject = OpenAPIObject['components']['schemas']['key']

export class PersonGet {}

export const personSchemaObject: SchemaObject = {
  oneOf: [
    {
      $ref: getSchemaPath(CompanyGet),
    },
    {
      $ref: getSchemaPath(UserGet),
    },
  ],
  discriminator: {
    propertyName: '_resolveType',
    mapping: {
      user: getSchemaPath(UserGet),
      company: getSchemaPath(CompanyGet),
    },
  },
}
```

I would like to be able to add this to the generated `OpenApiObject` in a simple manner

## Expected behavior
<!-- Describe what the desired behavior would be. -->

The current swagger implementations provides the following function:

```ts
export declare class SwaggerModule {
  static createDocument(
    app: INestApplication,
    config: Omit<OpenAPIObject, 'paths'>,
    options?: SwaggerDocumentOptions
  ): OpenAPIObject;
}
```
Ideally, a component should be able to be added to `config`:

```ts
config: {
  components: {
    schemas: {
       PersonGet: personSchemaObject
    }
  }
}
```

Unfortunately, the passed components are not merged deeply:

```ts
    // internal @nestjs/swagger code, in swagger-module.ts
    const document = swaggerScanner.scanApplication(app, options);
    document.components = {
      ...(config.components || {}),
      ...document.components
    };
```

I see two ways to address the problem:

- Change the behavior of `SwaggerModule.createDocument` to merge `config.components` one level deeper
- Create a new class decorator e.g. `@ApiRawDefinition` which contains the raw definition of the model, applied to the model class. 

## What is the motivation / use case for changing the behavior?

I want to create a model that is a `oneOf` of two other models. This is not supported yet, so I want to be able to specify the raw openapi definition somewhere.

Others:

I would be willing to make a PR, but I need to know which solution is preferrable :)

Pull request:
feat(config): merge passed schemas, links, ... in createDocument
Merge the passed config's components one level deeper, so as to be able to
specify individual schemas that will not be overwritten by the generated document.

Fix #1244

## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [x] Docs have been added / updated (for bug fixes / features)


## PR Type
What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->
```
[ ] Bugfix
[x] Feature
[ ] Code style update (formatting, local variables)
[ ] Refactoring (no functional changes, no api changes)
[ ] Build related changes
[ ] CI related changes
[ ] Other... Please describe:
```

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

Issue Number: #1244 


## What is the new behavior?

`config.components` is merged one level deeper.

There is an alternative solution to #1244 which is to create a new decorator.

## Does this PR introduce a breaking change?
```
[x] Yes
[ ] No
```

Schemas, links, examples, responses, ... passed to `SwaggerModule.createDocument` via `config.components` are no longer overwritten if definitions of the same kind are present in the generated document.

They will be overwritten only if a definition of the same kind AND with the same name is present.

The amount of existing applications that would need to migrate should be relatively low, as the config will typically be handcrafted with nothing extra unwanted.


Patch:
diff --git a/lib/decorators/api-extension.decorator.ts b/lib/decorators/api-extension.decorator.ts
index 7e615d95..1b305a6e 100644
--- a/lib/decorators/api-extension.decorator.ts
+++ b/lib/decorators/api-extension.decorator.ts
@@ -1,6 +1,5 @@
 import { DECORATORS } from '../constants';
 import { createMixedDecorator } from './helpers';
-import { clone } from 'lodash';
 
 export function ApiExtension(extensionKey: string, extensionProperties: any) {
   if (!extensionKey.startsWith('x-')) {
@@ -10,7 +9,10 @@ export function ApiExtension(extensionKey: string, extensionProperties: any) {
   }
 
   const extensionObject = {
-    [extensionKey]: clone(extensionProperties)
+    [extensionKey]:
+      typeof extensionProperties !== 'string'
+        ? { ...extensionProperties }
+        : extensionProperties
   };
 
   return createMixedDecorator(DECORATORS.API_EXTENSION, extensionObject);
diff --git a/lib/decorators/api-security.decorator.ts b/lib/decorators/api-security.decorator.ts
index 46c82b9b..f93725b1 100644
--- a/lib/decorators/api-security.decorator.ts
+++ b/lib/decorators/api-security.decorator.ts
@@ -1,19 +1,12 @@
-import { isString } from 'lodash';
 import { DECORATORS } from '../constants';
 import { SecurityRequirementObject } from '../interfaces/open-api-spec.interface';
 import { extendMetadata } from '../utils/extend-metadata.util';
 
 export function ApiSecurity(
-  name: string | SecurityRequirementObject,
+  name: string,
   requirements: string[] = []
 ): ClassDecorator & MethodDecorator {
-  let metadata: SecurityRequirementObject[];
-
-  if (isString(name)) {
-    metadata = [{ [name]: requirements }];
-  } else {
-    metadata = [name];
-  }
+  let metadata: SecurityRequirementObject[] = [{ [name]: requirements }];
 
   return (
     target: object,
diff --git a/lib/document-builder.ts b/lib/document-builder.ts
index 4e578931..f2d78581 100644
--- a/lib/document-builder.ts
+++ b/lib/document-builder.ts
@@ -1,10 +1,9 @@
 import { Logger } from '@nestjs/common';
-import { isString, isUndefined, negate, pickBy } from 'lodash';
+import { isUndefined, negate, pickBy } from 'lodash';
 import { buildDocumentBase } from './fixtures/document.base';
 import { OpenAPIObject } from './interfaces';
 import {
   ExternalDocumentationObject,
-  SecurityRequirementObject,
   SecuritySchemeObject,
   ServerVariableObject,
   TagObject
@@ -92,19 +91,11 @@ export class DocumentBuilder {
   }
 
   public addSecurityRequirements(
-    name: string | SecurityRequirementObject,
+    name: string,
     requirements: string[] = []
   ): this {
-    let securityRequirement: SecurityRequirementObject;
-
-    if (isString(name)) {
-      securityRequirement = { [name]: requirements };
-    } else {
-      securityRequirement = name;
-    }
-
     this.document.security = (this.document.security || []).concat({
-      ...securityRequirement
+      [name]: requirements
     });
     return this;
   }
@@ -182,7 +173,7 @@ export class DocumentBuilder {
     return this;
   }
 
-  public build(): Omit<OpenAPIObject, 'components' | 'paths'> {
+  public build(): Omit<OpenAPIObject, 'paths'> {
     return this.document;
   }
 }
diff --git a/lib/plugin/utils/plugin-utils.ts b/lib/plugin/utils/plugin-utils.ts
index 99611721..fc08e26d 100644
--- a/lib/plugin/utils/plugin-utils.ts
+++ b/lib/plugin/utils/plugin-utils.ts
@@ -111,8 +111,6 @@ export function hasPropertyKey(
     .some((item) => item.name.getText() === key);
 }
 
-const NATIVE_NODEJS_LIBRARIES = 'stream';
-
 export function replaceImportPath(typeReference: string, fileName: string) {
   if (!typeReference.includes('import')) {
     return typeReference;
@@ -124,37 +122,33 @@ export function replaceImportPath(typeReference: string, fileName: string) {
   importPath = convertPath(importPath);
   importPath = importPath.slice(2, importPath.length - 1);
 
-  if (NATIVE_NODEJS_LIBRARIES.includes(importPath)) {
-    return typeReference.replace('import', 'require');
-  } else {
-    let relativePath = posix.relative(posix.dirname(fileName), importPath);
-    relativePath = relativePath[0] !== '.' ? './' + relativePath : relativePath;
+  let relativePath = posix.relative(posix.dirname(fileName), importPath);
+  relativePath = relativePath[0] !== '.' ? './' + relativePath : relativePath;
+
+  const nodeModulesText = 'node_modules';
+  const nodeModulePos = relativePath.indexOf(nodeModulesText);
+  if (nodeModulePos >= 0) {
+    relativePath = relativePath.slice(
+      nodeModulePos + nodeModulesText.length + 1 // slash
+    );
 
-    const nodeModulesText = 'node_modules';
-    const nodeModulePos = relativePath.indexOf(nodeModulesText);
-    if (nodeModulePos >= 0) {
+    const typesText = '@types';
+    const typesPos = relativePath.indexOf(typesText);
+    if (typesPos >= 0) {
       relativePath = relativePath.slice(
-        nodeModulePos + nodeModulesText.length + 1 // slash
+        typesPos + typesText.length + 1 //slash
       );
-
-      const typesText = '@types';
-      const typesPos = relativePath.indexOf(typesText);
-      if (typesPos >= 0) {
-        relativePath = relativePath.slice(
-          typesPos + typesText.length + 1 //slash
-        );
-      }
-
-      const indexText = '/index';
-      const indexPos = relativePath.indexOf(indexText);
-      if (indexPos >= 0) {
-        relativePath = relativePath.slice(0, indexPos);
-      }
     }
 
-    typeReference = typeReference.replace(importPath, relativePath);
-    return typeReference.replace('import', 'require');
+    const indexText = '/index';
+    const indexPos = relativePath.indexOf(indexText);
+    if (indexPos >= 0) {
+      relativePath = relativePath.slice(0, indexPos);
+    }
   }
+
+  typeReference = typeReference.replace(importPath, relativePath);
+  return typeReference.replace('import', 'require');
 }
 
 export function isDynamicallyAdded(identifier: ts.Node) {
diff --git a/lib/services/schema-object-factory.ts b/lib/services/schema-object-factory.ts
index a7506db0..011c0a69 100644
--- a/lib/services/schema-object-factory.ts
+++ b/lib/services/schema-object-factory.ts
@@ -8,8 +8,7 @@ import {
   keyBy,
   mapValues,
   omit,
-  omitBy,
-  pick
+  omitBy
 } from 'lodash';
 import { DECORATORS } from '../constants';
 import { getTypeIsArrayTuple } from '../decorators/helpers';
@@ -425,22 +424,15 @@ export class SchemaObjectFactory {
     type: string | Record<string, any>
   ): SchemaObjectMetadata {
     const keysToRemove = ['type', 'enum'];
-    const keysToMove = [
-      'format',
-      'maximum',
-      'maxLength',
-      'minimum',
-      'minLength',
-      'pattern'
-    ];
-    const movedProperties = pick(metadata, keysToMove);
     const schemaHost = {
-      ...omit(metadata, [...keysToRemove, ...keysToMove]),
+      ...omit(metadata, keysToRemove),
       name: metadata.name || key,
       type: 'array',
       items: isString(type)
-        ? { type, ...movedProperties }
-        : { ...type, ...movedProperties }
+        ? {
+            type
+          }
+        : { ...type }
     };
     schemaHost.items = omitBy(schemaHost.items, isUndefined);
     return schemaHost as unknown;
diff --git a/lib/swagger-module.ts b/lib/swagger-module.ts
index 89fd3ddc..4769c089 100644
--- a/lib/swagger-module.ts
+++ b/lib/swagger-module.ts
@@ -6,6 +6,7 @@ import {
   SwaggerDocumentOptions
 } from './interfaces';
 import { SwaggerScanner } from './swagger-scanner';
+import { assignTwoLevelsDeep } from './utils/assign-two-levels-deep';
 import { validatePath } from './utils/validate-path.util';
 
 export class SwaggerModule {
@@ -16,10 +17,13 @@ export class SwaggerModule {
   ): OpenAPIObject {
     const swaggerScanner = new SwaggerScanner();
     const document = swaggerScanner.scanApplication(app, options);
-    document.components = {
-      ...(config.components || {}),
-      ...document.components
-    };
+
+    document.components = assignTwoLevelsDeep(
+      {},
+      config.components,
+      document.components
+    );
+
     return {
       openapi: '3.0.0',
       ...config,
diff --git a/lib/utils/assign-two-levels-deep.ts b/lib/utils/assign-two-levels-deep.ts
new file mode 100644
index 00000000..a2682e89
--- /dev/null
+++ b/lib/utils/assign-two-levels-deep.ts
@@ -0,0 +1,27 @@
+/**
+ * Merge one level deeper than a regular Object.assign().
+ *
+ * @example
+ *
+ * ```
+ * const a = {foo: {bar: 1, baz: 2, bag: {x : 1}}};
+ * const b = {foo: {baz: 3, bag: {y: 2}}};
+ *
+ * assignTwoLevelsDeep(a, b);
+ *
+ * // a is {foo: {bar: 1, baz: 3, bag: {y: 2}}}
+ * ```
+ */
+export function assignTwoLevelsDeep<TObject, T>(_dest: TObject, ...args: T[]) {
+  const dest = _dest as TObject & T;
+
+  for (const arg of args) {
+    for (const [key, value] of Object.entries(arg ?? {}) as Array<
+      [keyof T, T[keyof T]]
+    >) {
+      dest[key] = { ...dest[key], ...value };
+    }
+  }
+
+  return dest;
+}
diff --git a/package-lock.json b/package-lock.json
index 71154990..863ed507 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "@nestjs/swagger",
-  "version": "4.8.0",
+  "version": "4.7.16",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
@@ -20,7 +20,7 @@
         "@nestjs/core": "7.6.14",
         "@nestjs/platform-express": "7.6.14",
         "@nestjs/platform-fastify": "7.6.14",
-        "@types/jest": "26.0.21",
+        "@types/jest": "26.0.20",
         "@types/lodash": "4.14.168",
         "@types/node": "11.15.49",
         "@typescript-eslint/eslint-plugin": "4.18.0",
@@ -40,7 +40,7 @@
         "release-it": "14.4.1",
         "swagger-parser": "10.0.2",
         "swagger-ui-express": "4.1.6",
-        "ts-jest": "26.5.4",
+        "ts-jest": "26.5.3",
         "typescript": "4.2.3"
       },
       "peerDependencies": {
@@ -2924,9 +2924,9 @@
       }
     },
     "node_modules/@types/jest": {
-      "version": "26.0.21",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.21.tgz",
-      "integrity": "sha512-ab9TyM/69yg7eew9eOwKMUmvIZAKEGZYlq/dhe5/0IMUd/QLJv5ldRMdddSn+u22N13FP3s5jYyktxuBwY0kDA==",
+      "version": "26.0.20",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.20.tgz",
+      "integrity": "sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==",
       "dev": true,
       "dependencies": {
         "jest-diff": "^26.0.0",
@@ -16441,9 +16441,9 @@
       }
     },
     "node_modules/ts-jest": {
-      "version": "26.5.4",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.4.tgz",
-      "integrity": "sha512-I5Qsddo+VTm94SukBJ4cPimOoFZsYTeElR2xy6H2TOVs+NsvgYglW8KuQgKoApOKuaU/Ix/vrF9ebFZlb5D2Pg==",
+      "version": "26.5.3",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.3.tgz",
+      "integrity": "sha512-nBiiFGNvtujdLryU7MiMQh1iPmnZ/QvOskBbD2kURiI1MwqvxlxNnaAB/z9TbslMqCsSbu5BXvSSQPc5tvHGeA==",
       "dev": true,
       "dependencies": {
         "bs-logger": "0.x",
@@ -19714,9 +19714,9 @@
       }
     },
     "@types/jest": {
-      "version": "26.0.21",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.21.tgz",
-      "integrity": "sha512-ab9TyM/69yg7eew9eOwKMUmvIZAKEGZYlq/dhe5/0IMUd/QLJv5ldRMdddSn+u22N13FP3s5jYyktxuBwY0kDA==",
+      "version": "26.0.20",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.20.tgz",
+      "integrity": "sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA==",
       "dev": true,
       "requires": {
         "jest-diff": "^26.0.0",
@@ -30092,9 +30092,9 @@
       "dev": true
     },
     "ts-jest": {
-      "version": "26.5.4",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.4.tgz",
-      "integrity": "sha512-I5Qsddo+VTm94SukBJ4cPimOoFZsYTeElR2xy6H2TOVs+NsvgYglW8KuQgKoApOKuaU/Ix/vrF9ebFZlb5D2Pg==",
+      "version": "26.5.3",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.3.tgz",
+      "integrity": "sha512-nBiiFGNvtujdLryU7MiMQh1iPmnZ/QvOskBbD2kURiI1MwqvxlxNnaAB/z9TbslMqCsSbu5BXvSSQPc5tvHGeA==",
       "dev": true,
       "requires": {
         "bs-logger": "0.x",
diff --git a/package.json b/package.json
index 03b5bc63..437b8921 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@nestjs/swagger",
-  "version": "4.8.0",
+  "version": "4.7.16",
   "description": "Nest - modern, fast, powerful node.js web framework (@swagger)",
   "author": "Kamil Mysliwiec",
   "license": "MIT",
@@ -31,7 +31,7 @@
     "@nestjs/core": "7.6.14",
     "@nestjs/platform-express": "7.6.14",
     "@nestjs/platform-fastify": "7.6.14",
-    "@types/jest": "26.0.21",
+    "@types/jest": "26.0.20",
     "@types/lodash": "4.14.168",
     "@types/node": "11.15.49",
     "@typescript-eslint/eslint-plugin": "4.18.0",
@@ -51,7 +51,7 @@
     "release-it": "14.4.1",
     "swagger-parser": "10.0.2",
     "swagger-ui-express": "4.1.6",
-    "ts-jest": "26.5.4",
+    "ts-jest": "26.5.3",
     "typescript": "4.2.3"
   },
   "peerDependencies": {


Test patch:
diff --git a/e2e/api-spec.json b/e2e/api-spec.json
index 476c0dcb..f0f6550c 100644
--- a/e2e/api-spec.json
+++ b/e2e/api-spec.json
@@ -33,16 +33,6 @@
         "in": "header",
         "name": "api_key"
       },
-      "key1": {
-        "type": "apiKey",
-        "in": "header",
-        "name": "key1"
-      },
-      "key2": {
-        "type": "apiKey",
-        "in": "header",
-        "name": "key2"
-      },
       "cookie": {
         "type": "apiKey",
         "in": "cookie",
@@ -102,8 +92,7 @@
           "tags": {
             "type": "array",
             "items": {
-              "type": "string",
-              "format": "uri"
+              "type": "string"
             }
           },
           "createdAt": {
@@ -240,10 +229,6 @@
   "security": [
     {
       "bearer": []
-    },
-    {
-      "basic": [],
-      "cookie": []
     }
   ],
   "paths": {
@@ -296,10 +281,6 @@
           "create cats"
         ],
         "security": [
-          {
-            "key2": [],
-            "key1": []
-          },
           {
             "bearer": []
           },
@@ -310,12 +291,6 @@
       },
       "get": {
         "operationId": "CatsController_findAll",
-        "x-codeSamples": [
-          {
-            "lang": "JavaScript",
-            "source": "console.log('Hello World');"
-          }
-        ],
         "parameters": [
           {
             "name": "header",
@@ -408,10 +383,6 @@
           "cats"
         ],
         "security": [
-          {
-            "key2": [],
-            "key1": []
-          },
           {
             "bearer": []
           },
@@ -461,10 +432,6 @@
           "cats"
         ],
         "security": [
-          {
-            "key2": [],
-            "key1": []
-          },
           {
             "bearer": []
           },
@@ -569,10 +536,6 @@
           "cats"
         ],
         "security": [
-          {
-            "key2": [],
-            "key1": []
-          },
           {
             "bearer": []
           },
@@ -606,10 +569,6 @@
           "cats"
         ],
         "security": [
-          {
-            "key2": [],
-            "key1": []
-          },
           {
             "bearer": []
           },
@@ -654,10 +613,6 @@
           "cats"
         ],
         "security": [
-          {
-            "key2": [],
-            "key1": []
-          },
           {
             "bearer": []
           },
@@ -712,10 +667,6 @@
           "cats"
         ],
         "security": [
-          {
-            "key2": [],
-            "key1": []
-          },
           {
             "bearer": []
           },
@@ -749,10 +700,6 @@
           "cats"
         ],
         "security": [
-          {
-            "key2": [],
-            "key1": []
-          },
           {
             "bearer": []
           },
diff --git a/e2e/fastify.e2e-spec.ts b/e2e/fastify.e2e-spec.ts
index 95bc5319..5b04022e 100644
--- a/e2e/fastify.e2e-spec.ts
+++ b/e2e/fastify.e2e-spec.ts
@@ -27,11 +27,8 @@ describe('Fastify Swagger', () => {
       .addBearerAuth()
       .addOAuth2()
       .addApiKey()
-      .addApiKey({ type: 'apiKey' }, 'key1')
-      .addApiKey({ type: 'apiKey' }, 'key2')
       .addCookieAuth()
-      .addSecurityRequirements('bearer')
-      .addSecurityRequirements({ basic: [], cookie: [] });
+      .addSecurityRequirements('bearer');
   });
 
   it('should produce a valid OpenAPI 3.0 schema', async () => {
diff --git a/e2e/src/cats/cats.controller.ts b/e2e/src/cats/cats.controller.ts
index c60ddc60..923abbff 100644
--- a/e2e/src/cats/cats.controller.ts
+++ b/e2e/src/cats/cats.controller.ts
@@ -17,7 +17,6 @@ import { PaginationQuery } from './dto/pagination-query.dto';
 
 @ApiSecurity('basic')
 @ApiBearerAuth()
-@ApiSecurity({ key2: [], key1: [] })
 @ApiTags('cats')
 @ApiHeader({
   name: 'header',
@@ -55,9 +54,6 @@ export class CatsController {
   }
 
   @Get()
-  @ApiExtension('x-codeSamples', [
-    { lang: 'JavaScript', source: "console.log('Hello World');" }
-  ])
   findAll(@Query() paginationQuery: PaginationQuery) {}
 
   @ApiQuery({ type: PaginationQuery })
diff --git a/e2e/src/cats/dto/create-cat.dto.ts b/e2e/src/cats/dto/create-cat.dto.ts
index acc33a05..0e688b5a 100644
--- a/e2e/src/cats/dto/create-cat.dto.ts
+++ b/e2e/src/cats/dto/create-cat.dto.ts
@@ -15,7 +15,6 @@ export class CreateCatDto {
   readonly breed: string;
 
   @ApiProperty({
-    format: 'uri',
     type: [String]
   })
   readonly tags?: string[];
diff --git a/e2e/validate-schema.e2e-spec.ts b/e2e/validate-schema.e2e-spec.ts
index 836c5daf..0e32f8e7 100644
--- a/e2e/validate-schema.e2e-spec.ts
+++ b/e2e/validate-schema.e2e-spec.ts
@@ -1,20 +1,30 @@
+import { INestApplication } from '@nestjs/common';
 import { NestFactory } from '@nestjs/core';
 import { writeFileSync } from 'fs';
 import { join } from 'path';
 import * as SwaggerParser from 'swagger-parser';
-import { DocumentBuilder, OpenAPIObject, SwaggerModule } from '../lib';
+import {
+  DocumentBuilder,
+  getSchemaPath,
+  OpenAPIObject,
+  SwaggerModule
+} from '../lib';
 import { ApplicationModule } from './src/app.module';
+import { Cat } from './src/cats/classes/cat.class';
+import { TagDto } from './src/cats/dto/tag.dto';
+import type { OpenAPIV3 } from 'openapi-types';
 
 describe('Validate OpenAPI schema', () => {
-  let document: OpenAPIObject;
+  let app: INestApplication;
+  let options: Omit<OpenAPIObject, 'paths'>;
 
   beforeEach(async () => {
-    const app = await NestFactory.create(ApplicationModule, {
+    app = await NestFactory.create(ApplicationModule, {
       logger: false
     });
     app.setGlobalPrefix('api/');
 
-    const options = new DocumentBuilder()
+    options = new DocumentBuilder()
       .setTitle('Cats example')
       .setDescription('The cats API description')
       .setVersion('1.0')
@@ -24,17 +34,14 @@ describe('Validate OpenAPI schema', () => {
       .addBearerAuth()
       .addOAuth2()
       .addApiKey()
-      .addApiKey({ type: 'apiKey' }, 'key1')
-      .addApiKey({ type: 'apiKey' }, 'key2')
       .addCookieAuth()
       .addSecurityRequirements('bearer')
-      .addSecurityRequirements({ basic: [], cookie: [] })
       .build();
-
-    document = SwaggerModule.createDocument(app, options);
   });
 
   it('should produce a valid OpenAPI 3.0 schema', async () => {
+    const document = SwaggerModule.createDocument(app, options);
+
     const doc = JSON.stringify(document, null, 2);
     writeFileSync(join(__dirname, 'api-spec.json'), doc);
 
@@ -46,12 +53,48 @@ describe('Validate OpenAPI schema', () => {
         api.info.version
       );
       expect(api.info.title).toEqual('Cats example');
-      expect(api.paths['/api/cats']['get']['x-codeSamples'][0]['lang']).toEqual(
-        'JavaScript'
-      );
     } catch (err) {
       console.log(doc);
       expect(err).toBeUndefined();
     }
   });
+
+  it('should merge custom components passed via config', async () => {
+    const components = {
+      schemas: {
+        Person: {
+          oneOf: [
+            {
+              $ref: getSchemaPath(Cat)
+            },
+            {
+              $ref: getSchemaPath(TagDto)
+            }
+          ],
+          discriminator: {
+            propertyName: '_resolveType',
+            mapping: {
+              cat: getSchemaPath(Cat),
+              tag: getSchemaPath(TagDto)
+            }
+          }
+        }
+      }
+    };
+
+    const document = SwaggerModule.createDocument(app, {
+      ...options,
+      components: {
+        ...options.components,
+        ...components
+      }
+    });
+
+    let api = (await SwaggerParser.validate(
+      document as any
+    )) as OpenAPIV3.Document;
+    console.log('API name: %s, Version: %s', api.info.title, api.info.version);
+    expect(api.components.schemas).toHaveProperty('Person');
+    expect(api.components.schemas).toHaveProperty('Cat');
+  });
 });
diff --git a/test/services/fixtures/create-user.dto.ts b/test/services/fixtures/create-user.dto.ts
index 0771e43a..f48366ce 100644
--- a/test/services/fixtures/create-user.dto.ts
+++ b/test/services/fixtures/create-user.dto.ts
@@ -46,8 +46,7 @@ export class CreateUserDto {
 
   @ApiProperty({
     type: String,
-    isArray: true,
-    format: 'uri'
+    isArray: true
   })
   urls: string[];
 
diff --git a/test/services/schema-object-factory.spec.ts b/test/services/schema-object-factory.spec.ts
index d61d7435..d580fce1 100644
--- a/test/services/schema-object-factory.spec.ts
+++ b/test/services/schema-object-factory.spec.ts
@@ -148,7 +148,6 @@ describe('SchemaObjectFactory', () => {
           },
           urls: {
             items: {
-              format: 'uri',
               type: 'string'
             },
             type: 'array'