Repository: nestjs/swagger (ts)

Issue:
CLI plugin should support nullable
## 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
```typescript
{
  nullableValOpt?: string | null;
}
```
the type of the field will be `object`:  
![image](https://user-images.githubusercontent.com/20422904/90646363-6bed1680-e237-11ea-99aa-87ab44b92801.png)

## Expected behavior
- the type should be `string `
- `nullable `should be `true`  
![image](https://user-images.githubusercontent.com/20422904/90646674-c4bcaf00-e237-11ea-934b-98d62f535368.png)


## Environment

Nest version: `@nestjs/swagger": "^4.5.12"`

Pull request description:
feat(@nestjs/swagger): CLI plugin now supports nullable
`nullableValOpt?: string | null;` now results in `type`=`string`, `nullable`=`true`
before this change, the type was object

this fixes #912

## 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
[ ] 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: N/A


## What is the new behavior?


## Does this PR introduce a breaking change?
```
[ ] Yes
[ ] 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/plugin/visitors/model-class.visitor.ts b/lib/plugin/visitors/model-class.visitor.ts
index 8df70b92..eaae5a31 100644
--- a/lib/plugin/visitors/model-class.visitor.ts
+++ b/lib/plugin/visitors/model-class.visitor.ts
@@ -123,13 +123,12 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     hostFilename = ''
   ): ts.ObjectLiteralExpression {
     const isRequired = !node.questionToken;
-
     let properties = [
       ...existingProperties,
       !hasPropertyKey('required', existingProperties) &&
         ts.createPropertyAssignment('required', ts.createLiteral(isRequired)),
-      this.createTypePropertyAssignment(
-        node,
+      ...this.createTypePropertyAssignments(
+        node.type,
         typeChecker,
         existingProperties,
         hostFilename
@@ -151,37 +150,97 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     return objectLiteral;
   }
 
-  createTypePropertyAssignment(
-    node: ts.PropertyDeclaration | ts.PropertySignature,
+  /**
+   * The function returns an array with 0, 1 or 2 PropertyAssignments.
+   * Possible keys:
+   * - 'type'
+   * - 'nullable'
+   */
+  private createTypePropertyAssignments(
+    node: ts.TypeNode,
     typeChecker: ts.TypeChecker,
     existingProperties: ts.NodeArray<ts.PropertyAssignment>,
     hostFilename: string
-  ) {
+  ): ts.PropertyAssignment[] {
     const key = 'type';
     if (hasPropertyKey(key, existingProperties)) {
-      return undefined;
-    }
-    const type = typeChecker.getTypeAtLocation(node);
-    if (!type) {
-      return undefined;
+      return [];
     }
-    if (node.type && ts.isTypeLiteralNode(node.type)) {
-      const propertyAssignments = Array.from(node.type.members || []).map(
-        (member) => {
-          const literalExpr = this.createDecoratorObjectLiteralExpr(
-            member as ts.PropertySignature,
+    if (node) {
+      if (ts.isTypeLiteralNode(node)) {
+        const propertyAssignments = Array.from(node.members || []).map(
+          (member) => {
+            const literalExpr = this.createDecoratorObjectLiteralExpr(
+              member as ts.PropertySignature,
+              typeChecker,
+              existingProperties,
+              {},
+              hostFilename
+            );
+            return ts.createPropertyAssignment(
+              ts.createIdentifier(member.name.getText()),
+              literalExpr
+            );
+          }
+        );
+        return [
+          ts.createPropertyAssignment(
+            key,
+            ts.createArrowFunction(
+              undefined,
+              undefined,
+              [],
+              undefined,
+              undefined,
+              ts.createParen(ts.createObjectLiteral(propertyAssignments))
+            )
+          )
+        ];
+      } else if (ts.isUnionTypeNode(node)) {
+        const nullableType = node.types.find(
+          (type) => type.kind === ts.SyntaxKind.NullKeyword
+        );
+        const isNullable = !!nullableType;
+        const remainingTypes = node.types.filter(
+          (item) => item !== nullableType
+        );
+        /**
+         * when we have more than 1 type left, we could use oneOf
+         */
+        if (remainingTypes.length === 1) {
+          const remainingTypesProperties = this.createTypePropertyAssignments(
+            remainingTypes[0],
             typeChecker,
             existingProperties,
-            {},
             hostFilename
           );
-          return ts.createPropertyAssignment(
-            ts.createIdentifier(member.name.getText()),
-            literalExpr
+
+          const resultArray = new Array<ts.PropertyAssignment>(
+            ...remainingTypesProperties
           );
+          if (isNullable) {
+            const nullablePropertyAssignment = ts.createPropertyAssignment(
+              'nullable',
+              ts.createTrue()
+            );
+            resultArray.push(nullablePropertyAssignment);
+          }
+          return resultArray;
         }
-      );
-      return ts.createPropertyAssignment(
+      }
+    }
+
+    const type = typeChecker.getTypeAtLocation(node);
+    if (!type) {
+      return [];
+    }
+    let typeReference = getTypeReferenceAsString(type, typeChecker);
+    if (!typeReference) {
+      return [];
+    }
+    typeReference = replaceImportPath(typeReference, hostFilename);
+    return [
+      ts.createPropertyAssignment(
         key,
         ts.createArrowFunction(
           undefined,
@@ -189,26 +248,10 @@ export class ModelClassVisitor extends AbstractFileVisitor {
           [],
           undefined,
           undefined,
-          ts.createParen(ts.createObjectLiteral(propertyAssignments))
+          ts.createIdentifier(typeReference)
         )
-      );
-    }
-    let typeReference = getTypeReferenceAsString(type, typeChecker);
-    if (!typeReference) {
-      return undefined;
-    }
-    typeReference = replaceImportPath(typeReference, hostFilename);
-    return ts.createPropertyAssignment(
-      key,
-      ts.createArrowFunction(
-        undefined,
-        undefined,
-        [],
-        undefined,
-        undefined,
-        ts.createIdentifier(typeReference)
       )
-    );
+    ];
   }
 
   createEnumPropertyAssignment(
diff --git a/package-lock.json b/package-lock.json
index 5e176b39..9f2c0772 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9645,9 +9645,9 @@
       }
     },
     "lodash": {
-      "version": "4.17.20",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
-      "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
+      "version": "4.17.19",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
+      "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
     },
     "lodash._reinterpolate": {
       "version": "3.0.0",
diff --git a/package.json b/package.json
index 03ad6fe3..49ea26d0 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
   },
   "dependencies": {
     "@nestjs/mapped-types": "0.0.5",
-    "lodash": "4.17.20",
+    "lodash": "4.17.19",
     "path-to-regexp": "3.2.0"
   },
   "devDependencies": {


Test patch:
diff --git a/test/plugin/fixtures/nullable.dto.ts b/test/plugin/fixtures/nullable.dto.ts
new file mode 100644
index 00000000..4357dc82
--- /dev/null
+++ b/test/plugin/fixtures/nullable.dto.ts
@@ -0,0 +1,21 @@
+export const nullableDtoText = `
+export class NullableDto {
+  @ApiProperty()
+  stringValue: string | null;
+  @ApiProperty()
+  stringArr: string[] | null;
+}
+`;
+
+export const nullableDtoTextTranspiled = `export class NullableDto {
+    static _OPENAPI_METADATA_FACTORY() {
+        return { stringValue: { required: true, type: () => String, nullable: true }, stringArr: { required: true, type: () => [String], nullable: true } };
+    }
+}
+__decorate([
+    ApiProperty()
+], NullableDto.prototype, "stringValue", void 0);
+__decorate([
+    ApiProperty()
+], NullableDto.prototype, "stringArr", void 0);
+`;
diff --git a/test/plugin/model-class-visitor.spec.ts b/test/plugin/model-class-visitor.spec.ts
index 08ad2994..c3e22026 100644
--- a/test/plugin/model-class-visitor.spec.ts
+++ b/test/plugin/model-class-visitor.spec.ts
@@ -16,8 +16,33 @@ import {
   es5CreateCatDtoText,
   es5CreateCatDtoTextTranspiled
 } from './fixtures/es5-class.dto';
+import {
+  nullableDtoText,
+  nullableDtoTextTranspiled
+} from './fixtures/nullable.dto';
 
 describe('API model properties', () => {
+  it('should understand nullable', () => {
+    const options: ts.CompilerOptions = {
+      module: ts.ModuleKind.ESNext,
+      target: ts.ScriptTarget.ESNext,
+      newLine: ts.NewLineKind.LineFeed,
+      noEmitHelpers: true,
+      strict: true
+    };
+    const filename = 'nullable.dto.ts';
+    const fakeProgram = ts.createProgram([filename], options);
+
+    const result = ts.transpileModule(nullableDtoText, {
+      compilerOptions: options,
+      fileName: filename,
+      transformers: {
+        before: [before({ classValidatorShim: true }, fakeProgram)]
+      }
+    });
+    expect(result.outputText).toEqual(nullableDtoTextTranspiled);
+  });
+
   it('should add the metadata factory when no decorators exist', () => {
     const options: ts.CompilerOptions = {
       module: ts.ModuleKind.ESNext,