Repository: nestjs/swagger
Pull request #585 (2020-02-24 11:12:07)

auto generate description from comments
## 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)  
test added.
- [x] Docs have been added / updated (for bug fixes / features)  
no docs in this project yet.


## 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. -->

When enable the plugin by modified nest-cli.json , there is no default `description` and `example` in `ApiProperty` and `ApiOperation`

## What is the new behavior?

Auto generate `description` and (`example` or `examples`) from comment, just like this:
```
export abstract class Audit {
  /** test on createdAt */
  @CreateDateColumn()
  createdAt;

  // test on updatedAt1
  // test on updatedAt2
  @UpdateDateColumn()
  updatedAt;

  /**
   * test
   * version 
   * @example '0.0.1'
   * @memberof Audit
   */
  @VersionColumn()
  version;

  /**
   * testVersion
   *  
   * @example '0.0.1'
   * @example '0.0.2'
   * @memberof Audit
   */
  testVersion;
}
``` 
=>
```
export class Audit {
    static _OPENAPI_METADATA_FACTORY() {
        return { createdAt: { description: "test on createdAt", required: true, type: () => Object }, updatedAt: { description: "test on updatedAt1\\ntest on updatedAt2", required: true, type: () => Object }, version: { description: "test\\nversion", required: true, type: () => Object, example: "0.0.1" }, testVersion: { description: "testVersion", required: true, type: () => Object, examples: ["0.0.1", "0.0.2"] } };
    }
}
```

## Does this PR introduce a breaking change?
```
[ ] Yes
[x] No
```

## Other information

BenWildeman: @qbcbyb Could I make a suggestion and remove the `// normal comments` support. if feels wrong to use these also as these aren't documentation comments. this ties in better with TS and JSDoc spec
kamilmysliwiec: Thanks you for contribution. However, I'm not entirely sure why we should even extract comments to enhance the specification. These are 2 completely separate areas. I would recommend publishing this a separate plugin (separate NPM package).
BenWildeman: @kamilmysliwiec it would have been good to have a discussion around this instead of straight up closing it. right now, in my project, we have to duplicate the comment for both swagger and JS docs, whereas, this PR would have only required one. the whole point of the swagger plugin you created was to limit the code duplication 
![image](https://user-images.githubusercontent.com/3674467/76316274-392b1380-62d2-11ea-9f00-3af884ba8163.png)

kamilmysliwiec: As I've pointed out above, you can always publish a separate plugin - CLI allows applying multiple plugins at once.
BenWildeman: Your plugin misses the mark when you have to still use `ApiProperty` for specifying the description, but sure, more plugins it is
qbcbyb: > @qbcbyb Could I make a suggestion and remove the `// normal comments` support. if feels wrong to use these as these aren't documentation comments. this ties in better with TS and JSDoc spec. it could potentially show a commented out line in the documentation which is obviously undesired

Hav removed the `// normal comments` support. And publish in npm: [@qbcbyb/nestjs_swagger_plugin](https://www.npmjs.com/package/@qbcbyb/nestjs_swagger_plugin)
sjakos: @kamilmysliwiec is this PR lined up to be merged in soon?

Patch:
diff --git a/.gitignore b/.gitignore
index f73e3568..cb72281e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
 /.idea
 /.awcache
 /.vscode
+.history
 
 # misc
 npm-debug.log
diff --git a/lib/plugin/compiler-plugin.ts b/lib/plugin/compiler-plugin.ts
index fd7d358c..84ae76c4 100644
--- a/lib/plugin/compiler-plugin.ts
+++ b/lib/plugin/compiler-plugin.ts
@@ -17,7 +17,7 @@ export const before = (options?: Record<string, any>, program?: ts.Program) => {
         return modelClassVisitor.visit(sf, ctx, program, options);
       }
       if (isFilenameMatched(options.controllerFileNameSuffix, sf.fileName)) {
-        return controllerClassVisitor.visit(sf, ctx, program);
+        return controllerClassVisitor.visit(sf, ctx, program, options);
       }
       return sf;
     };
diff --git a/lib/plugin/merge-options.ts b/lib/plugin/merge-options.ts
index a5061b43..48adcbdb 100644
--- a/lib/plugin/merge-options.ts
+++ b/lib/plugin/merge-options.ts
@@ -4,12 +4,16 @@ export interface PluginOptions {
   dtoFileNameSuffix?: string | string[];
   controllerFileNameSuffix?: string | string[];
   classValidatorShim?: boolean;
+  dtoKeyOfComment?: string;
+  controllerKeyOfComment?: string;
 }
 
 const defaultOptions: PluginOptions = {
   dtoFileNameSuffix: ['.dto.ts', '.entity.ts'],
   controllerFileNameSuffix: ['.controller.ts'],
-  classValidatorShim: true
+  classValidatorShim: true,
+  dtoKeyOfComment: 'description',
+  controllerKeyOfComment: 'description'
 };
 
 export const mergePluginOptions = (
diff --git a/lib/plugin/utils/ast-utils.ts b/lib/plugin/utils/ast-utils.ts
index 3ab75d05..341930e7 100644
--- a/lib/plugin/utils/ast-utils.ts
+++ b/lib/plugin/utils/ast-utils.ts
@@ -11,7 +11,11 @@ import {
   Type,
   TypeChecker,
   TypeFlags,
-  TypeFormatFlags
+  TypeFormatFlags,
+  SourceFile,
+  getTrailingCommentRanges,
+  getLeadingCommentRanges,
+  CommentRange
 } from 'typescript';
 import { isDynamicallyAdded } from './plugin-utils';
 
@@ -98,6 +102,47 @@ export function getDefaultTypeFormatFlags(enclosingNode: Node) {
   return formatFlags;
 }
 
+export function getMainCommentAndExamplesOfNode(
+  node: Node,
+  sourceFile: SourceFile,
+  needExamples?: boolean
+): [string, string[]] {
+  const sourceText = sourceFile.getFullText();
+
+  const replaceRegex = /^ *\** *@.*$|^ *\/\*+ *|^ *\/\/+.*|^ *\/+ *|^ *\*+ *| +$| *\**\/ *$/gim;
+
+  const commentResult = [];
+  const examplesResult = [];
+  const extractCommentsAndExamples = (comments?: CommentRange[]) =>
+    comments?.forEach(comment => {
+      const commentSource = sourceText.substring(comment.pos, comment.end);
+      const oneComment = commentSource.replace(replaceRegex, '').trim();
+      if (oneComment) {
+        commentResult.push(oneComment);
+      }
+      if (needExamples) {
+        const regexOfExample = /@example *['"]?([^ ]+?)['"]? *$/gim;
+        let execResult: RegExpExecArray;
+        while (
+          (execResult = regexOfExample.exec(commentSource)) &&
+          execResult.length > 1
+        ) {
+          examplesResult.push(execResult[1]);
+        }
+      }
+    });
+  extractCommentsAndExamples(
+    getLeadingCommentRanges(sourceText, node.getFullStart())
+  );
+  if (!commentResult.length) {
+    extractCommentsAndExamples(
+      getTrailingCommentRanges(sourceText, node.getFullStart())
+    );
+  }
+
+  return [commentResult.join('\n'), examplesResult];
+}
+
 export function getDecoratorArguments(decorator: Decorator) {
   const callExpression = decorator.expression;
   return (callExpression && (callExpression as CallExpression).arguments) || [];
diff --git a/lib/plugin/visitors/controller-class.visitor.ts b/lib/plugin/visitors/controller-class.visitor.ts
index 592d9b6a..891f9d69 100644
--- a/lib/plugin/visitors/controller-class.visitor.ts
+++ b/lib/plugin/visitors/controller-class.visitor.ts
@@ -1,8 +1,11 @@
 import { compact, head } from 'lodash';
 import * as ts from 'typescript';
-import { ApiResponse } from '../../decorators';
+import { ApiResponse, ApiOperation } from '../../decorators';
 import { OPENAPI_NAMESPACE } from '../plugin-constants';
-import { getDecoratorArguments } from '../utils/ast-utils';
+import {
+  getDecoratorArguments,
+  getMainCommentAndExamplesOfNode
+} from '../utils/ast-utils';
 import {
   getDecoratorOrUndefinedByNames,
   getTypeReferenceAsString,
@@ -10,19 +13,27 @@ import {
   replaceImportPath
 } from '../utils/plugin-utils';
 import { AbstractFileVisitor } from './abstract.visitor';
+import { PluginOptions } from '../merge-options';
 
 export class ControllerClassVisitor extends AbstractFileVisitor {
   visit(
     sourceFile: ts.SourceFile,
     ctx: ts.TransformationContext,
-    program: ts.Program
+    program: ts.Program,
+    options: PluginOptions
   ) {
     const typeChecker = program.getTypeChecker();
     sourceFile = this.updateImports(sourceFile);
 
     const visitNode = (node: ts.Node): ts.Node => {
       if (ts.isMethodDeclaration(node)) {
-        return this.addDecoratorToNode(node, typeChecker, sourceFile.fileName);
+        return this.addDecoratorToNode(
+          node,
+          typeChecker,
+          options,
+          sourceFile.fileName,
+          sourceFile
+        );
       }
       return ts.visitEachChild(node, visitNode, ctx);
     };
@@ -32,17 +43,23 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
   addDecoratorToNode(
     compilerNode: ts.MethodDeclaration,
     typeChecker: ts.TypeChecker,
-    hostFilename: string
+    options: PluginOptions,
+    hostFilename: string,
+    sourceFile: ts.SourceFile
   ): ts.MethodDeclaration {
     const node = ts.getMutableClone(compilerNode);
-    if (!node.decorators) {
-      return compilerNode;
-    }
-    const { pos, end } = node.decorators;
+    const nodeArray = node.decorators || ts.createNodeArray();
+    const { pos, end } = nodeArray;
 
     node.decorators = Object.assign(
       [
-        ...node.decorators,
+        ...this.createApiOperationOrEmptyInArray(
+          node,
+          nodeArray,
+          options,
+          sourceFile
+        ),
+        ...nodeArray,
         ts.createDecorator(
           ts.createCall(
             ts.createIdentifier(`${OPENAPI_NAMESPACE}.${ApiResponse.name}`),
@@ -63,6 +80,57 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
     return node;
   }
 
+  createApiOperationOrEmptyInArray(
+    node: ts.MethodDeclaration,
+    nodeArray: ts.NodeArray<ts.Decorator>,
+    options: PluginOptions,
+    sourceFile: ts.SourceFile
+  ) {
+    const keyToGenerate = options.controllerKeyOfComment;
+    const apiOperationDecorator = getDecoratorOrUndefinedByNames(
+      [ApiOperation.name],
+      nodeArray
+    );
+    let apiOperationOptions: ts.ObjectLiteralExpression;
+    let apiOperationOptionsProperties: ts.NodeArray<ts.PropertyAssignment>;
+    let comments;
+    if (
+      // No ApiOperation or No ApiOperationOptions or ApiOperationOptions is empty or No description in ApiOperationOptions
+      (!apiOperationDecorator ||
+        !(apiOperationOptions = head(
+          getDecoratorArguments(apiOperationDecorator)
+        )) ||
+        !(apiOperationOptionsProperties = apiOperationOptions.properties as ts.NodeArray<
+          ts.PropertyAssignment
+        >) ||
+        !hasPropertyKey(keyToGenerate, apiOperationOptionsProperties)) &&
+      // Has comments
+      ([comments] = getMainCommentAndExamplesOfNode(node, sourceFile))[0]
+    ) {
+      const properties = [
+        ts.createPropertyAssignment(keyToGenerate, ts.createLiteral(comments)),
+        ...(apiOperationOptionsProperties ?? ts.createNodeArray())
+      ];
+      const apiOperationDecoratorArguments: ts.NodeArray<ts.Expression> = ts.createNodeArray(
+        [ts.createObjectLiteral(compact(properties))]
+      );
+      if (apiOperationDecorator) {
+        (apiOperationDecorator.expression as ts.CallExpression).arguments = apiOperationDecoratorArguments;
+      } else {
+        return [
+          ts.createDecorator(
+            ts.createCall(
+              ts.createIdentifier(`${OPENAPI_NAMESPACE}.${ApiOperation.name}`),
+              undefined,
+              apiOperationDecoratorArguments
+            )
+          )
+        ];
+      }
+    }
+    return [];
+  }
+
   createDecoratorObjectLiteralExpr(
     node: ts.MethodDeclaration,
     typeChecker: ts.TypeChecker,
diff --git a/lib/plugin/visitors/model-class.visitor.ts b/lib/plugin/visitors/model-class.visitor.ts
index 8df70b92..9b433292 100644
--- a/lib/plugin/visitors/model-class.visitor.ts
+++ b/lib/plugin/visitors/model-class.visitor.ts
@@ -1,9 +1,18 @@
 import { compact, flatten, head } from 'lodash';
 import * as ts from 'typescript';
-import { ApiHideProperty } from '../../decorators';
+import {
+  ApiHideProperty,
+  ApiProperty,
+  ApiPropertyOptional
+} from '../../decorators';
 import { PluginOptions } from '../merge-options';
 import { METADATA_FACTORY_NAME } from '../plugin-constants';
-import { getDecoratorArguments, getText, isEnum } from '../utils/ast-utils';
+import {
+  getDecoratorArguments,
+  getText,
+  isEnum,
+  getMainCommentAndExamplesOfNode
+} from '../utils/ast-utils';
 import {
   extractTypeArgumentIfArray,
   getDecoratorOrUndefinedByNames,
@@ -40,6 +49,23 @@ export class ModelClassVisitor extends AbstractFileVisitor {
         if (hidePropertyDecorator) {
           return node;
         }
+
+        let apiOperationOptionsProperties: ts.NodeArray<ts.PropertyAssignment>;
+        const apiPropertyDecorator = getDecoratorOrUndefinedByNames(
+          [ApiProperty.name, ApiPropertyOptional.name],
+          decorators
+        );
+        if (apiPropertyDecorator) {
+          apiOperationOptionsProperties = head(
+            getDecoratorArguments(apiPropertyDecorator)
+          )?.properties;
+          node.decorators = ts.createNodeArray([
+            ...node.decorators.filter(
+              decorator => decorator != apiPropertyDecorator
+            )
+          ]);
+        }
+
         const isPropertyStatic = (node.modifiers || []).some(
           (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword
         );
@@ -51,6 +77,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
             node,
             typeChecker,
             options,
+            apiOperationOptionsProperties ?? ts.createNodeArray(),
             sourceFile.fileName,
             sourceFile
           );
@@ -100,15 +127,17 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     compilerNode: ts.PropertyDeclaration,
     typeChecker: ts.TypeChecker,
     options: PluginOptions,
+    existingProperties: ts.NodeArray<ts.PropertyAssignment>,
     hostFilename: string,
     sourceFile: ts.SourceFile
   ) {
     const objectLiteralExpr = this.createDecoratorObjectLiteralExpr(
       compilerNode,
       typeChecker,
-      ts.createNodeArray(),
+      existingProperties,
       options,
-      hostFilename
+      hostFilename,
+      sourceFile
     );
     this.addClassMetadata(compilerNode, objectLiteralExpr, sourceFile);
   }
@@ -120,12 +149,52 @@ export class ModelClassVisitor extends AbstractFileVisitor {
       ts.PropertyAssignment
     > = ts.createNodeArray(),
     options: PluginOptions = {},
-    hostFilename = ''
+    hostFilename = '',
+    sourceFile?: ts.SourceFile
   ): ts.ObjectLiteralExpression {
     const isRequired = !node.questionToken;
 
+    const descriptionPropertyWapper = [];
+    const examplesPropertyWapper = [];
+    if (sourceFile) {
+      const [comments, examples] = getMainCommentAndExamplesOfNode(
+        node,
+        sourceFile,
+        true
+      );
+      const keyOfComment = options?.dtoKeyOfComment ??'description';
+      if (!hasPropertyKey(keyOfComment, existingProperties) && comments) {
+        descriptionPropertyWapper.push(
+          ts.createPropertyAssignment(keyOfComment, ts.createLiteral(comments))
+        );
+      }
+      if (
+        !(
+          hasPropertyKey('example', existingProperties) ||
+          hasPropertyKey('examples', existingProperties)
+        ) &&
+        examples.length
+      ) {
+        if (examples.length == 1) {
+          examplesPropertyWapper.push(
+            ts.createPropertyAssignment(
+              'example',
+              ts.createLiteral(examples[0])
+            )
+          );
+        } else {
+          examplesPropertyWapper.push(
+            ts.createPropertyAssignment(
+              'examples',
+              ts.createArrayLiteral(examples.map(e => ts.createLiteral(e)))
+            )
+          );
+        }
+      }
+    }
     let properties = [
       ...existingProperties,
+      ...descriptionPropertyWapper,
       !hasPropertyKey('required', existingProperties) &&
         ts.createPropertyAssignment('required', ts.createLiteral(isRequired)),
       this.createTypePropertyAssignment(
@@ -134,6 +203,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
         existingProperties,
         hostFilename
       ),
+      ...examplesPropertyWapper,
       this.createDefaultPropertyAssignment(node, existingProperties),
       this.createEnumPropertyAssignment(
         node,


Test patch:
diff --git a/test/plugin/controller-class-visitor.spec.ts b/test/plugin/controller-class-visitor.spec.ts
index 5cc4fbfc..168c7ded 100644
--- a/test/plugin/controller-class-visitor.spec.ts
+++ b/test/plugin/controller-class-visitor.spec.ts
@@ -19,7 +19,9 @@ describe('Controller methods', () => {
     const result = ts.transpileModule(appControllerText, {
       compilerOptions: options,
       fileName: filename,
-      transformers: { before: [before({}, fakeProgram)] }
+      transformers: {
+        before: [before({ controllerKeyOfComment: 'summary' }, fakeProgram)]
+      }
     });
     expect(result.outputText).toEqual(appControllerTextTranspiled);
   });
diff --git a/test/plugin/fixtures/app.controller.ts b/test/plugin/fixtures/app.controller.ts
index 9af2f8c4..2e15dfb4 100644
--- a/test/plugin/fixtures/app.controller.ts
+++ b/test/plugin/fixtures/app.controller.ts
@@ -1,12 +1,35 @@
 export const appControllerText = `import { Controller, Post, HttpStatus } from '@nestjs/common';
+import { ApiOperation } from '@nestjs/swagger';
 
 class Cat {}
 
 @Controller('cats')
 export class AppController {
+  /**
+   * create a Cat
+   *
+   * @returns {Promise<Cat>}
+   * @memberof AppController
+   */
   @Post()
   async create(): Promise<Cat> {}
 
+  /**
+   * find a Cat
+   */
+  @ApiOperation({})
+  @Get()
+  async findOne(): Promise<Cat> {}
+
+  /**
+   * find all Cats im comment
+   *
+   * @returns {Promise<Cat>}
+   * @memberof AppController
+   */
+  @ApiOperation({
+    description: 'find all Cats',
+  })
   @Get()
   @HttpCode(HttpStatus.NO_CONTENT)
   async findAll(): Promise<Cat[]> {}
@@ -17,17 +40,41 @@ Object.defineProperty(exports, \"__esModule\", { value: true });
 exports.AppController = void 0;
 const openapi = require(\"@nestjs/swagger\");
 const common_1 = require(\"@nestjs/common\");
+const swagger_1 = require("@nestjs/swagger");
 class Cat {
 }
 let AppController = class AppController {
+    /**
+     * create a Cat
+     *
+     * @returns {Promise<Cat>}
+     * @memberof AppController
+     */
     async create() { }
+    /**
+     * find a Cat
+     */
+    async findOne() { }
+    /**
+     * find all Cats im comment
+     *
+     * @returns {Promise<Cat>}
+     * @memberof AppController
+     */
     async findAll() { }
 };
 __decorate([
+    openapi.ApiOperation({ summary: "create a Cat" }),
     common_1.Post(),
     openapi.ApiResponse({ status: 201, type: Cat })
 ], AppController.prototype, \"create\", null);
 __decorate([
+    swagger_1.ApiOperation({ summary: "find a Cat" }),
+    Get(),
+    openapi.ApiResponse({ status: 200, type: Cat })
+], AppController.prototype, \"findOne\", null);
+__decorate([
+    swagger_1.ApiOperation({ summary: "find all Cats im comment", description: 'find all Cats' }),
     Get(),
     HttpCode(common_1.HttpStatus.NO_CONTENT),
     openapi.ApiResponse({ status: common_1.HttpStatus.NO_CONTENT, type: [Cat] })
diff --git a/test/plugin/fixtures/create-cat-alt.dto.ts b/test/plugin/fixtures/create-cat-alt.dto.ts
index fe18eb53..2f6806bf 100644
--- a/test/plugin/fixtures/create-cat-alt.dto.ts
+++ b/test/plugin/fixtures/create-cat-alt.dto.ts
@@ -25,6 +25,7 @@ export class CreateCatDto2 {
   readonly breed?: string | undefined;
   nodes: Node[];
   alias: AliasedType;
+  /** NumberAlias */
   numberAlias: NumberAlias;
   union: 1 | 2;
   intersection: Function & string;
@@ -55,7 +56,7 @@ export class CreateCatDto2 {
         this.status = Status.ENABLED;
     }
     static _OPENAPI_METADATA_FACTORY() {
-        return { name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, breed: { required: false, type: () => String }, nodes: { required: true, type: () => [Object] }, alias: { required: true, type: () => Object }, numberAlias: { required: true, type: () => Number }, union: { required: true, type: () => Object }, intersection: { required: true, type: () => Object }, nested: { required: true, type: () => ({ first: { required: true, type: () => String }, second: { required: true, type: () => Number }, status: { required: true, enum: Status }, tags: { required: true, type: () => [String] }, nodes: { required: true, type: () => [Object] }, alias: { required: true, type: () => Object }, numberAlias: { required: true, type: () => Number } }) } };
+        return { name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, breed: { required: false, type: () => String }, nodes: { required: true, type: () => [Object] }, alias: { required: true, type: () => Object }, numberAlias: { description: "NumberAlias", required: true, type: () => Number }, union: { required: true, type: () => Object }, intersection: { required: true, type: () => Object }, nested: { required: true, type: () => ({ first: { required: true, type: () => String }, second: { required: true, type: () => Number }, status: { required: true, enum: Status }, tags: { required: true, type: () => [String] }, nodes: { required: true, type: () => [Object] }, alias: { required: true, type: () => Object }, numberAlias: { required: true, type: () => Number } }) } };
     }
 }
 __decorate([
diff --git a/test/plugin/fixtures/create-cat-alt2.dto.ts b/test/plugin/fixtures/create-cat-alt2.dto.ts
index f4090f02..da0e81ab 100644
--- a/test/plugin/fixtures/create-cat-alt2.dto.ts
+++ b/test/plugin/fixtures/create-cat-alt2.dto.ts
@@ -2,21 +2,39 @@ export const createCatDtoAlt2Text = `
 import { CreateDateColumn, UpdateDateColumn, VersionColumn } from 'typeorm';
 
 export abstract class Audit {
+  /** test on createdAt */
   @CreateDateColumn()
   createdAt;
 
+  // test on updatedAt1
+  // test on updatedAt2
   @UpdateDateColumn()
   updatedAt;
 
+  /**
+   * test
+   * version 
+   * @example '0.0.1'
+   * @memberof Audit
+   */
   @VersionColumn()
   version;
+
+  /**
+   * testVersion
+   *  
+   * @example '0.0.1'
+   * @example '0.0.2'
+   * @memberof Audit
+   */
+  testVersion;
 }
 `;
 
 export const createCatDtoTextAlt2Transpiled = `import { CreateDateColumn, UpdateDateColumn, VersionColumn } from 'typeorm';
 export class Audit {
     static _OPENAPI_METADATA_FACTORY() {
-        return { createdAt: { required: true, type: () => Object }, updatedAt: { required: true, type: () => Object }, version: { required: true, type: () => Object } };
+        return { createdAt: { description: "test on createdAt", required: true, type: () => Object }, updatedAt: { required: true, type: () => Object }, version: { description: "test\\nversion", required: true, type: () => Object, example: "0.0.1" }, testVersion: { description: "testVersion", required: true, type: () => Object, examples: ["0.0.1", "0.0.2"] } };
     }
 }
 __decorate([
diff --git a/test/plugin/fixtures/create-cat.dto.ts b/test/plugin/fixtures/create-cat.dto.ts
index 4c80345e..e953f3cc 100644
--- a/test/plugin/fixtures/create-cat.dto.ts
+++ b/test/plugin/fixtures/create-cat.dto.ts
@@ -26,7 +26,8 @@ export class CreateCatDto {
   oneValueEnum?: OneValueEnum;
   oneValueEnumArr?: OneValueEnum[];
 
-  @ApiProperty({ type: String })
+  /** this is breed im comment */
+  @ApiProperty({ description: "this is breed", type: String })
   @IsString()
   readonly breed?: string;
 
@@ -57,7 +58,7 @@ export class CreateCatDto {
         this.status = Status.ENABLED;
     }
     static _OPENAPI_METADATA_FACTORY() {
-        return { name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3, minimum: 0, maximum: 10 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, status2: { required: false, enum: Status }, statusArr: { required: false, enum: Status, isArray: true }, oneValueEnum: { required: false, enum: OneValueEnum }, oneValueEnumArr: { required: false, enum: OneValueEnum }, breed: { required: false, type: () => String }, nodes: { required: true, type: () => [Object] }, optionalBoolean: { required: false, type: () => Boolean }, date: { required: true, type: () => Date } };
+        return { name: { required: true, type: () => String }, age: { required: true, type: () => Number, default: 3, minimum: 0, maximum: 10 }, tags: { required: true, type: () => [String] }, status: { required: true, default: Status.ENABLED, enum: Status }, status2: { required: false, enum: Status }, statusArr: { required: false, enum: Status, isArray: true }, oneValueEnum: { required: false, enum: OneValueEnum }, oneValueEnumArr: { required: false, enum: OneValueEnum }, breed: { description: "this is breed", type: String, title: "this is breed im comment", required: false }, nodes: { required: true, type: () => [Object] }, optionalBoolean: { required: false, type: () => Boolean }, date: { required: true, type: () => Date } };
     }
 }
 __decorate([
@@ -65,7 +66,6 @@ __decorate([
     Max(10)
 ], CreateCatDto.prototype, \"age\", void 0);
 __decorate([
-    ApiProperty({ type: String }),
     IsString()
 ], CreateCatDto.prototype, \"breed\", void 0);
 __decorate([
diff --git a/test/plugin/fixtures/es5-class.dto.ts b/test/plugin/fixtures/es5-class.dto.ts
index 297aa3f0..b878ea38 100644
--- a/test/plugin/fixtures/es5-class.dto.ts
+++ b/test/plugin/fixtures/es5-class.dto.ts
@@ -4,7 +4,9 @@ import { Status } from './status';
 import { CONSTANT_STRING, CONSTANT_OBJECT, MIN_VAL } from './constants';
 
 export class CreateCatDtoEs5 {
+  // field name
   name: string = CONSTANT_STRING;
+  /** status */
   status: Status = Status.ENABLED;
   obj = CONSTANT_OBJECT;
 
@@ -22,13 +24,15 @@ var status_1 = require(\"./status\");
 var constants_1 = require(\"./constants\");
 var CreateCatDtoEs5 = /** @class */ (function () {
     function CreateCatDtoEs5() {
+        // field name
         this.name = constants_1.CONSTANT_STRING;
+        /** status */
         this.status = status_1.Status.ENABLED;
         this.obj = constants_1.CONSTANT_OBJECT;
         this.age = 3;
     }
     CreateCatDtoEs5._OPENAPI_METADATA_FACTORY = function () {
-        return { name: { required: true, type: function () { return String; }, default: constants_1.CONSTANT_STRING }, status: { required: true, type: function () { return Object; }, default: status_1.Status.ENABLED }, obj: { required: true, type: function () { return Object; }, default: constants_1.CONSTANT_OBJECT }, age: { required: true, type: function () { return Number; }, default: 3, minimum: constants_1.MIN_VAL, maximum: 10 } };
+        return { name: { required: true, type: function () { return String; }, default: constants_1.CONSTANT_STRING }, status: { description: "status", required: true, type: function () { return Object; }, default: status_1.Status.ENABLED }, obj: { required: true, type: function () { return Object; }, default: constants_1.CONSTANT_OBJECT }, age: { required: true, type: function () { return Number; }, default: 3, minimum: constants_1.MIN_VAL, maximum: 10 } };
     };
     __decorate([
         Min(constants_1.MIN_VAL),
diff --git a/test/plugin/model-class-visitor.spec.ts b/test/plugin/model-class-visitor.spec.ts
index 08ad2994..a0cd795f 100644
--- a/test/plugin/model-class-visitor.spec.ts
+++ b/test/plugin/model-class-visitor.spec.ts
@@ -18,7 +18,7 @@ import {
 } from './fixtures/es5-class.dto';
 
 describe('API model properties', () => {
-  it('should add the metadata factory when no decorators exist', () => {
+  it('should add the metadata factory when no decorators exist, and generated propertyKey is title', () => {
     const options: ts.CompilerOptions = {
       module: ts.ModuleKind.ESNext,
       target: ts.ScriptTarget.ESNext,
@@ -33,7 +33,12 @@ describe('API model properties', () => {
       compilerOptions: options,
       fileName: filename,
       transformers: {
-        before: [before({ classValidatorShim: true }, fakeProgram)]
+        before: [
+          before(
+            { classValidatorShim: true, dtoKeyOfComment: 'title' },
+            fakeProgram
+          )
+        ]
       }
     });
     expect(result.outputText).toEqual(createCatDtoTextTranspiled);