Repository: nestjs/swagger
Pull request #412 (2019-12-04 17:42:24)

feature(): extract Enum out to its schema
## 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)
- [ ] 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. -->
Current behavior is when an `Enum` is defined and used on a `Model`, the `Enum` values are put on the `properties` of the `Model` itself.
<img width="303" alt="screen shot 2018-06-28 at 7 40 45 pm" src="https://user-images.githubusercontent.com/25516557/42067321-3982952a-7b0b-11e8-9fa3-bd6c9fb53b1d.png">

This causes Frontend Generation Tools (such as *NSwag*) to generate multiple `enums` even though they are meant for the same thing.
<img width="253" alt="screen shot 2018-06-28 at 7 38 12 pm" src="https://user-images.githubusercontent.com/25516557/42067341-5d9ea7aa-7b0b-11e8-9153-c6abf03e3e9d.png">

Issue Number: #372 


## What is the new behavior?
~~New behavior will expect: `enum` property on various decorator (`ApiParam`, `ApiQuery`, `ApiProperty`) to accept a lazy-function syntax.~~

~~This allows `SwaggerExplorer` to grab the Enum name at run-time which makes it possible to extract the Enum out to `schemas` to be reused on `Parameters`~~

~~Please note this is an **addition**, not **replacement**. `enum` property still accepts what it currently accepts. This PR makes it so it will solve the issue stated in `Current Behavior` with `lazy function` syntax.~~

Various decorators that accept `enum` now also accept `enumName`. If you want your `enum` to be store as a schema (reusable) then you need to provide `enumName` as well. This implementation might seem a little bit explicit compared to the `lazy load function` but its intention is also clearer. 

```typescript
// before
@ApiProperty({enum: Role})

// later
@ApiProperty({enum: Role, enumName: 'Role'})
```

Again, although the PR is working, maintain the current passing tests, passing  two new tests, my implementation might contain flaws and might go against NestJS's design mental. That said, I'm open to ALL suggestions and feedbacks.

## Does this PR introduce a breaking change?
```
[ ] Yes
[x] No
```

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->


## Other information


Patch:
diff --git a/lib/decorators/api-param.decorator.ts b/lib/decorators/api-param.decorator.ts
index f862f687..afbeedcf 100644
--- a/lib/decorators/api-param.decorator.ts
+++ b/lib/decorators/api-param.decorator.ts
@@ -13,6 +13,7 @@ type ParameterOptions = Omit<ParameterObject, 'in' | 'schema'>;
 interface ApiParamMetadata extends ParameterOptions {
   type?: Type<unknown> | Function | [Function] | string;
   enum?: SwaggerEnumType;
+  enumName?: string;
 }
 
 interface ApiParamSchemaHost extends ParameterOptions {
@@ -39,6 +40,12 @@ export function ApiParam(options: ApiParamOptions): MethodDecorator {
     const enumValues = getEnumValues((options as ApiParamMetadata).enum);
     (param.schema as SchemaObject).type = getEnumType(enumValues);
     (param.schema as SchemaObject).enum = enumValues;
+
+    if ((options as ApiParamMetadata).enumName) {
+      param['enumName'] = (options as ApiParamMetadata).enumName;
+    }
   }
+
+  !param['enumName'] && delete param['enumName'];
   return createParamDecorator(param, defaultParamOptions);
 }
diff --git a/lib/decorators/api-property.decorator.ts b/lib/decorators/api-property.decorator.ts
index c62ca383..434f3054 100644
--- a/lib/decorators/api-property.decorator.ts
+++ b/lib/decorators/api-property.decorator.ts
@@ -7,6 +7,7 @@ export interface ApiPropertyOptions
   extends Omit<SchemaObjectMetadata, 'name' | 'enum'> {
   name?: string;
   enum?: any[] | Record<string, any>;
+  enumName?: string;
 }
 
 const isEnumArray = (obj: ApiPropertyOptions): boolean =>
@@ -18,27 +19,6 @@ export function ApiProperty(
   return createApiPropertyDecorator(options);
 }
 
-export function ApiPropertyOptional(
-  options: ApiPropertyOptions = {}
-): PropertyDecorator {
-  return ApiProperty({
-    ...options,
-    required: false
-  });
-}
-
-export function ApiResponseProperty(
-  options: Pick<
-    ApiPropertyOptions,
-    'type' | 'example' | 'format' | 'enum' | 'deprecated'
-  > = {}
-): PropertyDecorator {
-  return ApiProperty({
-    readOnly: true,
-    ...options
-  });
-}
-
 export function createApiPropertyDecorator(
   options: ApiPropertyOptions = {},
   overrideExisting = true
@@ -65,9 +45,32 @@ export function createApiPropertyDecorator(
     options.enum = enumValues;
     options.type = getEnumType(enumValues);
   }
+
+  !options.enumName && delete options.enumName;
   return createPropertyDecorator(
     DECORATORS.API_MODEL_PROPERTIES,
     options,
     overrideExisting
   );
 }
+
+export function ApiPropertyOptional(
+  options: ApiPropertyOptions = {}
+): PropertyDecorator {
+  return ApiProperty({
+    ...options,
+    required: false
+  });
+}
+
+export function ApiResponseProperty(
+  options: Pick<
+    ApiPropertyOptions,
+    'type' | 'example' | 'format' | 'enum' | 'deprecated'
+  > = {}
+): PropertyDecorator {
+  return ApiProperty({
+    readOnly: true,
+    ...options
+  });
+}
diff --git a/lib/decorators/api-query.decorator.ts b/lib/decorators/api-query.decorator.ts
index 715cd161..d2cd0136 100644
--- a/lib/decorators/api-query.decorator.ts
+++ b/lib/decorators/api-query.decorator.ts
@@ -19,6 +19,7 @@ interface ApiQueryMetadata extends ParameterOptions {
   type?: Type<unknown> | Function | [Function] | string;
   isArray?: boolean;
   enum?: SwaggerEnumType;
+  enumName?: string;
 }
 
 interface ApiQuerySchemaHost extends ParameterOptions {
@@ -50,5 +51,7 @@ export function ApiQuery(options: ApiQueryOptions): MethodDecorator {
   } else if (isEnumDefined(options)) {
     addEnumSchema(param, options);
   }
+
+  !param['enumName'] && delete param['enumName'];
   return createParamDecorator(param, defaultQueryOptions);
 }
diff --git a/lib/explorers/api-parameters.explorer.ts b/lib/explorers/api-parameters.explorer.ts
index 1e355fac..d1e786de 100644
--- a/lib/explorers/api-parameters.explorer.ts
+++ b/lib/explorers/api-parameters.explorer.ts
@@ -24,6 +24,7 @@ const schemaObjectFactory = new SchemaObjectFactory(
 
 export const exploreApiParametersMetadata = (
   schemas: SchemaObject[],
+  schemaRefsStack: [],
   instance: object,
   prototype: Type<unknown>,
   method: Function
@@ -62,7 +63,8 @@ export const exploreApiParametersMetadata = (
 
   const paramsWithDefinitions = schemaObjectFactory.createFromModel(
     properties,
-    schemas
+    schemas,
+    schemaRefsStack
   );
   const parameters = swaggerTypesMapper.mapParamTypes(paramsWithDefinitions);
   return parameters ? { parameters } : undefined;
diff --git a/lib/interfaces/schema-object-metadata.interface.ts b/lib/interfaces/schema-object-metadata.interface.ts
index 1a4c330d..f61f13ae 100644
--- a/lib/interfaces/schema-object-metadata.interface.ts
+++ b/lib/interfaces/schema-object-metadata.interface.ts
@@ -7,4 +7,5 @@ export interface SchemaObjectMetadata
   isArray?: boolean;
   required?: boolean;
   name?: string;
+  enumName?: string;
 }
diff --git a/lib/services/parameter-metadata-accessor.ts b/lib/services/parameter-metadata-accessor.ts
index 1f8b851b..2af05fe1 100644
--- a/lib/services/parameter-metadata-accessor.ts
+++ b/lib/services/parameter-metadata-accessor.ts
@@ -21,6 +21,7 @@ export interface ParamWithTypeMetadata {
   isArray?: boolean;
   required: true;
   enum?: unknown[];
+  enumName?: string;
 }
 export type ParamsWithType = Record<string, ParamWithTypeMetadata>;
 
@@ -68,9 +69,8 @@ export class ParameterMetadataAccessor {
   private mapParamType(key: string): string {
     const keyPair = key.split(':');
     switch (Number(keyPair[0])) {
-      case RouteParamtypes.BODY: {
+      case RouteParamtypes.BODY:
         return 'body';
-      }
       case RouteParamtypes.PARAM:
         return 'path';
       case RouteParamtypes.QUERY:
diff --git a/lib/services/schema-object-factory.ts b/lib/services/schema-object-factory.ts
index da04fd65..b3a6702b 100644
--- a/lib/services/schema-object-factory.ts
+++ b/lib/services/schema-object-factory.ts
@@ -19,7 +19,13 @@ import {
 } from '../interfaces/open-api-spec.interface';
 import { SchemaObjectMetadata } from '../interfaces/schema-object-metadata.interface';
 import { getSchemaPath } from '../utils';
-import { getEnumType, getEnumValues, isEnumArray } from '../utils/enum.utils';
+import {
+  cleanUpParam,
+  isEnumMetadata,
+  getEnumType,
+  getEnumValues,
+  isEnumArray
+} from '../utils/enum.utils';
 import { isBodyParameter } from '../utils/is-body-parameter.util';
 import { isBuiltInType } from '../utils/is-built-in-type.util';
 import { isDateCtor } from '../utils/is-date-ctor.util';
@@ -35,10 +41,14 @@ export class SchemaObjectFactory {
 
   createFromModel(
     parameters: ParamWithTypeMetadata[],
-    schemas: SchemaObject[]
+    schemas: SchemaObject[],
+    schemaRefsStack: string[] = []
   ): Array<ParamWithTypeMetadata | BaseParameterObject> {
     return parameters.map(param => {
       if (!isBodyParameter(param)) {
+        if (param.enumName) {
+          return this.createEnumParam(param, schemas, schemaRefsStack);
+        }
         return param;
       }
       if (this.isPrimitiveType(param.type)) {
@@ -48,7 +58,11 @@ export class SchemaObjectFactory {
         return this.mapArrayCtorParam(param);
       }
 
-      const modelName = this.exploreModelSchema(param.type, schemas);
+      const modelName = this.exploreModelSchema(
+        param.type,
+        schemas,
+        schemaRefsStack
+      );
       const name = param.name || modelName;
       const schema = {
         ...((param as BaseParameterObject).schema || {}),
@@ -105,6 +119,7 @@ export class SchemaObjectFactory {
         schemas,
         schemaRefsStack
       );
+
       const schemaCombinators = ['oneOf', 'anyOf', 'allOf'];
       if (schemaCombinators.some(key => key in property)) {
         delete (property as SchemaObjectMetadata).type;
@@ -114,7 +129,7 @@ export class SchemaObjectFactory {
     const typeDefinition: SchemaObject = {
       type: 'object',
       properties: mapValues(keyBy(propertiesWithType, 'name'), property =>
-        omit(property, ['name', 'isArray', 'required'])
+        omit(property, ['name', 'isArray', 'required', 'enumName'])
       ) as Record<string, SchemaObject | ReferenceObject>
     };
     const typeDefinitionRequiredFields = (propertiesWithType as SchemaObjectMetadata[])
@@ -158,6 +173,15 @@ export class SchemaObjectFactory {
       );
     }
     if (isString(metadata.type)) {
+      if (isEnumMetadata(metadata)) {
+        return this.createEnumSchemaType(
+          key,
+          metadata,
+          schemas,
+          schemaRefsStack
+        );
+      }
+
       return {
         ...metadata,
         name: metadata.name || key
@@ -204,6 +228,84 @@ export class SchemaObjectFactory {
     };
   }
 
+  createEnumParam(
+    param: ParamWithTypeMetadata & BaseParameterObject,
+    schemas: SchemaObject[],
+    schemaRefsStack: string[]
+  ) {
+    let enumName = param.enumName;
+    const $ref = getSchemaPath(enumName);
+
+    if (!includes(schemaRefsStack, enumName)) {
+      schemaRefsStack.push(enumName);
+
+      const _enum = param.enum
+        ? param.enum
+        : param.schema['items']
+        ? param.schema['items']['enum']
+        : param.schema['enum'];
+
+      schemas.push({
+        [enumName]: {
+          type: 'string',
+          enum: _enum
+        }
+      });
+    }
+
+    param.schema =
+      param.isArray || param.schema?.['items']
+        ? { type: 'array', items: { $ref } }
+        : { $ref };
+
+    cleanUpParam(param);
+    return param;
+  }
+
+  createEnumSchemaType(
+    key: string,
+    metadata: SchemaObjectMetadata,
+    schemas: SchemaObject[],
+    schemaRefsStack: string[]
+  ): BaseParameterObject & Record<string, any> {
+    if (!metadata.enumName) {
+      return {
+        ...metadata,
+        name: metadata.name || key
+      } as any;
+    }
+
+    let enumName = metadata.enumName;
+    const $ref = getSchemaPath(enumName);
+
+    if (!includes(schemaRefsStack, enumName)) {
+      schemaRefsStack.push(enumName);
+      schemas.push({
+        [enumName]: {
+          type: 'string',
+          enum: metadata.isArray ? metadata.items['enum'] : metadata.enum
+        }
+      });
+    }
+
+    const _schemaObject = {
+      ...metadata,
+      name: metadata.name || key,
+      type: metadata.isArray ? 'array' : 'string'
+    };
+
+    const _ref = metadata.isArray ? { items: { $ref } } : { $ref };
+
+    const _paramObject = { ..._schemaObject, ..._ref };
+
+    if (!metadata.isArray) {
+      delete _paramObject.type;
+    }
+
+    delete _paramObject.enum;
+    return _paramObject as any;
+  }
+
   createNotBuiltInTypeReference(
     key: string,
     metadata: SchemaObjectMetadata,
diff --git a/lib/swagger-explorer.ts b/lib/swagger-explorer.ts
index 3be43876..c76f1481 100644
--- a/lib/swagger-explorer.ts
+++ b/lib/swagger-explorer.ts
@@ -55,6 +55,7 @@ export class SwaggerExplorer {
   private readonly mimetypeContentWrapper = new MimetypeContentWrapper();
   private readonly metadataScanner = new MetadataScanner();
   private readonly schemas: SchemaObject[] = [];
+  private readonly schemaRefsStack: string[] = [];
 
   constructor(private readonly schemaObjectFactory: SchemaObjectFactory) {}
 
@@ -69,7 +70,11 @@ export class SwaggerExplorer {
       root: [
         this.exploreRoutePathAndMethod,
         exploreApiOperationMetadata,
-        exploreApiParametersMetadata.bind(null, this.schemas)
+        exploreApiParametersMetadata.bind(
+          null,
+          this.schemas,
+          this.schemaRefsStack
+        )
       ],
       security: [exploreApiSecurityMetadata],
       tags: [exploreApiTagsMetadata],
diff --git a/lib/utils/enum.utils.ts b/lib/utils/enum.utils.ts
index 28f12041..700675b9 100644
--- a/lib/utils/enum.utils.ts
+++ b/lib/utils/enum.utils.ts
@@ -1,20 +1,28 @@
 import { isString } from 'lodash';
-import { SchemaObject } from '../interfaces/open-api-spec.interface';
+import {
+  BaseParameterObject,
+  SchemaObject
+} from '../interfaces/open-api-spec.interface';
+import { SchemaObjectMetadata } from '../interfaces/schema-object-metadata.interface';
+import { ParamWithTypeMetadata } from '../services/parameter-metadata-accessor';
 import { SwaggerEnumType } from '../types/swagger-enum.type';
 
 export function getEnumValues(enumType: SwaggerEnumType): string[] | number[] {
   if (Array.isArray(enumType)) {
     return enumType as string[];
   }
+
   if (typeof enumType !== 'object') {
     return [];
   }
+
   const values = [];
   const uniqueValues = {};
 
   for (const key in enumType) {
     const value = enumType[key];
-    // filter out cases where enum key also becomes its value (A: B, B: A)
+    // filter out cases where enum key also becomes its value (A: B, B: A)	    // filter out cases where enum key also
+    // becomes its value (A: B, B: A)
     if (
       !uniqueValues.hasOwnProperty(value) &&
       !uniqueValues.hasOwnProperty(key)
@@ -32,8 +40,8 @@ export function getEnumType(values: (string | number)[]): 'string' | 'number' {
 }
 
 export function addEnumArraySchema(
-  paramDefinition: Record<'schema' | 'isArray', any>,
-  decoratorOptions: Partial<Record<'enum', any>>
+  paramDefinition: Record<'schema' | 'isArray' | 'enumName', any>,
+  decoratorOptions: Partial<Record<'enum' | 'enumName', any>>
 ) {
   const paramSchema: SchemaObject = paramDefinition.schema || {};
   paramDefinition.schema = paramSchema;
@@ -45,11 +53,15 @@ export function addEnumArraySchema(
     type: getEnumType(enumValues),
     enum: enumValues
   };
+
+  if (decoratorOptions.enumName) {
+    paramDefinition.enumName = decoratorOptions.enumName;
+  }
 }
 
 export function addEnumSchema(
-  paramDefinition: Record<'schema', any>,
-  decoratorOptions: Partial<Record<'enum', any>>
+  paramDefinition: Record<'schema' | 'enumName', any>,
+  decoratorOptions: Partial<Record<'enum' | 'enumName', any>>
 ) {
   const paramSchema: SchemaObject = paramDefinition.schema || {};
   const enumValues = getEnumValues(decoratorOptions.enum);
@@ -57,8 +69,24 @@ export function addEnumSchema(
   paramDefinition.schema = paramSchema;
   paramSchema.enum = enumValues;
   paramSchema.type = getEnumType(enumValues);
+
+  if (decoratorOptions.enumName) {
+    paramDefinition.enumName = decoratorOptions.enumName;
+  }
 }
 
+export const cleanUpParam = (
+  param: ParamWithTypeMetadata & BaseParameterObject
+) => {
+  if (param.isArray) {
+    delete param.isArray;
+    delete param['items'];
+  }
+
+  delete param.enumName;
+  delete param.enum;
+};
+
 export const isEnumArray = <T extends Partial<Record<'isArray' | 'enum', any>>>(
   obj: Record<string, any>
 ): obj is T => obj.isArray && obj.enum;
@@ -66,3 +94,6 @@ export const isEnumArray = <T extends Partial<Record<'isArray' | 'enum', any>>>(
 export const isEnumDefined = <T extends Partial<Record<'enum', any>>>(
   obj: Record<string, any>
 ): obj is T => obj.enum;
+
+export const isEnumMetadata = (metadata: SchemaObjectMetadata) =>
+  metadata.enum || (metadata.isArray && metadata.items?.['enum']);


Test patch:
diff --git a/e2e/api-spec.json b/e2e/api-spec.json
index ae35f968..66795110 100644
--- a/e2e/api-spec.json
+++ b/e2e/api-spec.json
@@ -50,6 +50,14 @@
           "two"
         ]
       },
+      "LettersEnum": {
+        "type": "string",
+        "enum": [
+          "A",
+          "B",
+          "C"
+        ]
+      },
       "TagDto": {
         "type": "object",
         "properties": {
@@ -99,22 +107,12 @@
             }
           },
           "enum": {
-            "type": "string",
-            "enum": [
-              "A",
-              "B",
-              "C"
-            ]
+            "$ref": "#/components/schemas/LettersEnum"
           },
           "enumArr": {
             "type": "array",
             "items": {
-              "type": "string",
-              "enum": [
-                "A",
-                "B",
-                "C"
-              ]
+              "$ref": "#/components/schemas/LettersEnum"
             }
           },
           "tag": {
@@ -299,12 +297,7 @@
             "required": true,
             "in": "query",
             "schema": {
-              "enum": [
-                "A",
-                "B",
-                "C"
-              ],
-              "type": "string"
+              "$ref": "#/components/schemas/LettersEnum"
             }
           },
           {
@@ -314,12 +307,7 @@
             "schema": {
               "type": "array",
               "items": {
-                "type": "string",
-                "enum": [
-                  "A",
-                  "B",
-                  "C"
-                ]
+                "$ref": "#/components/schemas/LettersEnum"
               }
             }
           }
diff --git a/e2e/src/cats/dto/create-cat.dto.ts b/e2e/src/cats/dto/create-cat.dto.ts
index 888f50c5..c29398ff 100644
--- a/e2e/src/cats/dto/create-cat.dto.ts
+++ b/e2e/src/cats/dto/create-cat.dto.ts
@@ -42,12 +42,14 @@ export class CreateCatDto {
   readonly options?: Record<string, any>[];
 
   @ApiProperty({
-    enum: LettersEnum
+    enum: LettersEnum,
+    enumName: 'LettersEnum'
   })
   readonly enum: LettersEnum;
 
   @ApiProperty({
     enum: LettersEnum,
+    enumName: 'LettersEnum',
     isArray: true
   })
   readonly enumArr: LettersEnum;
diff --git a/e2e/src/cats/dto/pagination-query.dto.ts b/e2e/src/cats/dto/pagination-query.dto.ts
index 764fe9b1..55c1ed24 100644
--- a/e2e/src/cats/dto/pagination-query.dto.ts
+++ b/e2e/src/cats/dto/pagination-query.dto.ts
@@ -20,12 +20,14 @@ export class PaginationQuery {
   limit: number;
 
   @ApiProperty({
-    enum: LettersEnum
+    enum: LettersEnum,
+    enumName: 'LettersEnum'
   })
   enum: LettersEnum;
 
   @ApiProperty({
     enum: LettersEnum,
+    enumName: 'LettersEnum',
     isArray: true
   })
   enumArr: LettersEnum;
diff --git a/test/explorer/swagger-explorer.spec.ts b/test/explorer/swagger-explorer.spec.ts
index 00d6fb34..b4157d6e 100644
--- a/test/explorer/swagger-explorer.spec.ts
+++ b/test/explorer/swagger-explorer.spec.ts
@@ -481,6 +481,7 @@ describe('SwaggerExplorer', () => {
         return Promise.resolve([]);
       }
     }
+
     it('should merge implicit metadata with explicit options', () => {
       const explorer = new SwaggerExplorer(schemaObjectFactory);
       const routes = explorer.exploreController(
@@ -589,6 +590,12 @@ describe('SwaggerExplorer', () => {
       C = 'c'
     }
 
+    enum QueryEnum {
+      D = 'd',
+      E = 'e',
+      F = 'f'
+    }
+
     class Foo {}
 
     @Controller('')
@@ -598,16 +605,33 @@ describe('SwaggerExplorer', () => {
         name: 'objectId',
         enum: ParamEnum
       })
-      @ApiParam({
-        name: 'objectId',
-        enum: ParamEnum
-      })
-      @ApiQuery({ name: 'order', enum: ['d', 'e', 'f'] })
+      @ApiQuery({ name: 'order', enum: QueryEnum })
       @ApiQuery({ name: 'page', enum: ['d', 'e', 'f'], isArray: true })
       find(): Promise<Foo[]> {
         return Promise.resolve([]);
       }
     }
+
+    @Controller('')
+    class BarController {
+      @Get('bars/:objectId')
+      @ApiParam({
+        name: 'objectId',
+        enum: ParamEnum,
+        enumName: 'ParamEnum'
+      })
+      @ApiQuery({ name: 'order', enum: QueryEnum, enumName: 'QueryEnum' })
+      @ApiQuery({
+        name: 'page',
+        enum: QueryEnum,
+        enumName: 'QueryEnum',
+        isArray: true
+      })
+      findBar(): Promise<Foo> {
+        return Promise.resolve(null);
+      }
+    }
+
     it('should properly define enums', () => {
       const explorer = new SwaggerExplorer(schemaObjectFactory);
       const routes = explorer.exploreController(
@@ -651,6 +675,47 @@ describe('SwaggerExplorer', () => {
         }
       ]);
     });
+
+    it('should properly define enum as schema with lazy function', () => {
+      const explorer = new SwaggerExplorer(schemaObjectFactory);
+      const routes = explorer.exploreController(
+        {
+          instance: new BarController(),
+          metatype: BarController
+        } as InstanceWrapper<BarController>,
+        'path'
+      );
+
+      expect(routes[0].root.parameters).toEqual([
+        {
+          in: 'query',
+          name: 'page',
+          required: true,
+          schema: {
+            type: 'array',
+            items: {
+              $ref: '#/components/schemas/QueryEnum'
+            }
+          }
+        },
+        {
+          in: 'query',
+          name: 'order',
+          required: true,
+          schema: {
+            $ref: '#/components/schemas/QueryEnum'
+          }
+        },
+        {
+          in: 'path',
+          name: 'objectId',
+          required: true,
+          schema: {
+            $ref: '#/components/schemas/ParamEnum'
+          }
+        }
+      ]);
+    });
   });
 
   describe('when headers are defined', () => {
@@ -676,6 +741,7 @@ describe('SwaggerExplorer', () => {
         return Promise.resolve();
       }
     }
+
     it('should properly define headers', () => {
       const explorer = new SwaggerExplorer(schemaObjectFactory);
       const routes = explorer.exploreController(
diff --git a/test/services/schema-object-factory.spec.ts b/test/services/schema-object-factory.spec.ts
index 7450ebe5..6d471028 100644
--- a/test/services/schema-object-factory.spec.ts
+++ b/test/services/schema-object-factory.spec.ts
@@ -19,6 +19,66 @@ describe('SchemaObjectFactory', () => {
   });
 
   describe('exploreModelSchema', () => {
+    enum Role {
+      Admin = 'admin',
+      User = 'user'
+    }
+
+    class CreatePersonDto {
+      @ApiProperty()
+      name: string;
+      @ApiProperty({ enum: Role, enumName: 'Role' })
+      role: Role;
+    }
+
+    class Person {
+      @ApiProperty({ enum: Role, enumName: 'Role' })
+      role: Role;
+    }
+
+    it('should explore enum', () => {
+      const schemas = [];
+      schemaObjectFactory.exploreModelSchema(Person, schemas);
+
+      expect(schemas).toHaveLength(2);
+
+      expect(schemas[0]['Role']).toBeDefined();
+      expect(schemas[0]['Role']).toEqual({
+        type: 'string',
+        enum: ['admin', 'user']
+      });
+      expect(schemas[1]['Person']).toBeDefined();
+      expect(schemas[1]['Person']).toEqual({
+        type: 'object',
+        properties: {
+          role: {
+            $ref: '#/components/schemas/Role'
+          }
+        },
+        required: ['role']
+      });
+
+      schemaObjectFactory.exploreModelSchema(CreatePersonDto, schemas, [
+        'Person',
+        'Role'
+      ]);
+
+      expect(schemas).toHaveLength(3);
+      expect(schemas[2]['CreatePersonDto']).toBeDefined();
+      expect(schemas[2]['CreatePersonDto']).toEqual({
+        type: 'object',
+        properties: {
+          name: {
+            type: 'string'
+          },
+          role: {
+            $ref: '#/components/schemas/Role'
+          }
+        },
+        required: ['name', 'role']
+      });
+    });
+
     it('should create openapi schema', () => {
       const schemas = [];
       const schemaKey = schemaObjectFactory.exploreModelSchema(