Repository: nestjs/swagger (ts)

Issue:
BUG: Two dimensional array Not Support by CLI Plugin
<!--
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)-->
[x] Bug report
[ ] 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

I use [Cli Plugin](https://docs.nestjs.com/openapi/cli-plugin) that auto add `@ApiProperty`. My DTO:

```javascript
// ./dto/test-report.dto.ts
import { ValidateNested } from 'class-validator'
import { PlanExecutionDto } from './plan-execution.dto'
import { CreateTestReportDto } from './create-test-report.dto'
import { BugStatistics } from './bug-statistics.dto'
import { ManualPlanInfoDto } from './manual-plan-info.dto'
import { CaseExecutionDto } from './case-execution.dto'
import { ApiProperty } from '@nestjs/swagger'


export class TestReportDto {
  @ValidateNested()
  reportInfo!: CreateTestReportDto

  @ValidateNested()
  planInfo!: ManualPlanInfoDto

  @ValidateNested({ each: true })
  planExecution!: PlanExecutionDto[]

  // Here is the error line
  @ValidateNested({ each: true })
  caseExecution!: CaseExecutionDto[][]

  @ValidateNested()
  bugStatistics!: BugStatistics
}
```

The Swagger Output Piar:

```json

{
  "components": {
    "schemas": {
      "TestReportDto": {
        "type": "object",
        "properties": {
          "reportInfo": {
            "description": "报告信息",
            "allOf": [
              {
                "$ref": "#/components/schemas/CreateTestReportDto"
              }
            ]
          },
          "planInfo": {
            "description": "计划信息",
            "allOf": [
              {
                "$ref": "#/components/schemas/ManualPlanInfoDto"
              }
            ]
          },
          "planExecution": {
            "description": "计划/轮次执行情况",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanExecutionDto"
            }
          },
          "caseExecution": {
            "type": "array",
            "items": {
              // Here is the error
              "$ref": "#/components/schemas/"
            }
          },
          "bugStatistics": {
            "description": "Bug 统计",
            "allOf": [
              {
                "$ref": "#/components/schemas/BugStatistics"
              }
            ]
          }
        },
        "required": [
          "reportInfo",
          "planInfo",
          "planExecution",
          "caseExecution",
          "bugStatistics"
        ]
      }
    }
  }
}
```

## Expected behavior
<!-- Describe what the desired behavior would be. -->

The Swagger should be:

```json
{
  "components": {
    "schemas": {
      "TestReportDto": {
        "type": "object",
        "properties": {
          "reportInfo": {
            "description": "报告信息",
            "allOf": [
              {
                "$ref": "#/components/schemas/CreateTestReportDto"
              }
            ]
          },
          "planInfo": {
            "description": "计划信息",
            "allOf": [
              {
                "$ref": "#/components/schemas/ManualPlanInfoDto"
              }
            ]
          },
          "planExecution": {
            "description": "计划/轮次执行情况",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlanExecutionDto"
            }
          },
          // Here
          "caseExecution": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "$ref": "#/components/schemas/CaseExecutionDto"
              }
            }
          },
          "bugStatistics": {
            "description": "Bug 统计",
            "allOf": [
              {
                "$ref": "#/components/schemas/BugStatistics"
              }
            ]
          }
        },
        "required": [
          "reportInfo",
          "planInfo",
          "planExecution",
          "caseExecution",
          "bugStatistics"
        ]
      }
    }
  }
}
```


## Minimal reproduction of the problem with instructions

* Create a nestjs project with cli plugin.
* add a DTO with  Two dimensional array.
* Generate swagger

## What is the motivation / use case for changing the behavior?
<!-- Describe the motivation or the concrete use case. -->
Two-dimensional arrays are very common in data structures.


## Environment

<pre><code>
Nest version: X.Y.Z
<!-- Check whether this is still an issue in the most recent Nest version -->
 
For Tooling issues:
- Node version: v14.16.1
- Platform:  Mac

Others:

- @nestjs/core: 7.6.15
- @nestjs/swagger: 4.8.0
<!-- Anything else relevant?  Operating system version, IDE, package manager, ... -->
</code></pre>


Pull request:
feat(): support nested array type for ApiProperty
## PR Checklist
Please check if your PR fulfills the following requirements:

- [ ] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [ ] 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)
[x] 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. -->
SwaggerModule fails to build the correct schema for more than one level Array type.

```ts
export class Foo {
   @ApiProperty({ type: () => [[String]] })
   twoDimensionThings: string[][];
}
```
will output the following property:
```yaml
properties:
   twoDimensionThings:
      type: 'array'
      items:
         $ref: '#/components/schemas/'
```

The issue mentions the problem in the Plugin but it's the problem in the Schema factory.

Issue Number: #1339 

## What is the new behavior?
This PR will add support for nested array type for both using `ApiProperty` and using plugin.

```yaml
properties:
   twoDimensionThings:
      type: 'array'
      items:
         type: 'array'
         items:
            type: 'string'
   twoDimensionBar:
      type: 'array'
      items:
         type: 'array'
         items:
            type: '#/components/schemas/Bar'
```

This PR also refactors the plugin to use `ts.NodeFactory` instead of deprecated methods on `ts` namespace. 

## 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
fix #1339

Patch:
diff --git a/lib/decorators/api-property.decorator.ts b/lib/decorators/api-property.decorator.ts
index 9f4dbb0a..736b9335 100644
--- a/lib/decorators/api-property.decorator.ts
+++ b/lib/decorators/api-property.decorator.ts
@@ -46,6 +46,16 @@ export function createApiPropertyDecorator(
     options.type = getEnumType(enumValues);
   }
 
+  if (Array.isArray(options.type)) {
+    options.type = 'array';
+    options.items = {
+      type: 'array',
+      items: {
+        type: options.type[0]
+      }
+    }
+  }
+
   return createPropertyDecorator(
     DECORATORS.API_MODEL_PROPERTIES,
     options,
diff --git a/lib/plugin/utils/ast-utils.ts b/lib/plugin/utils/ast-utils.ts
index 544574d5..815e4aee 100644
--- a/lib/plugin/utils/ast-utils.ts
+++ b/lib/plugin/utils/ast-utils.ts
@@ -1,3 +1,4 @@
+import * as ts from 'typescript';
 import {
   CallExpression,
   CommentRange,
@@ -15,7 +16,9 @@ import {
   Type,
   TypeChecker,
   TypeFlags,
-  TypeFormatFlags
+  TypeFormatFlags,
+  TypeNode,
+  UnionTypeNode
 } from 'typescript';
 import { isDynamicallyAdded } from './plugin-utils';
 
@@ -206,3 +209,32 @@ function getNameFromExpression(expression: LeftHandSideExpression) {
   }
   return expression;
 }
+
+export function findNullableTypeFromUnion(
+  typeNode: UnionTypeNode,
+  typeChecker: TypeChecker
+) {
+  return typeNode.types.find((tNode: TypeNode) =>
+    hasFlag(typeChecker.getTypeAtLocation(tNode), TypeFlags.Null)
+  );
+}
+
+export function createBooleanLiteral(
+  factory: ts.NodeFactory,
+  flag: boolean
+): ts.BooleanLiteral {
+  return flag ? factory.createTrue() : factory.createFalse();
+}
+
+export function createPrimitiveLiteral(factory: ts.NodeFactory, item: unknown) {
+  const typeOfItem = typeof item;
+
+  switch (typeOfItem) {
+    case 'boolean':
+      return createBooleanLiteral(factory, item as boolean);
+    case 'number':
+      return factory.createNumericLiteral(item as number);
+    case 'string':
+      return factory.createStringLiteral(item as string);
+  }
+}
diff --git a/lib/plugin/visitors/controller-class.visitor.ts b/lib/plugin/visitors/controller-class.visitor.ts
index 3874cb2e..01c0415e 100644
--- a/lib/plugin/visitors/controller-class.visitor.ts
+++ b/lib/plugin/visitors/controller-class.visitor.ts
@@ -29,6 +29,7 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
       if (ts.isMethodDeclaration(node)) {
         try {
           return this.addDecoratorToNode(
+            ctx.factory,
             node,
             typeChecker,
             options,
@@ -45,50 +46,60 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
   }
 
   addDecoratorToNode(
+    factory: ts.NodeFactory,
     compilerNode: ts.MethodDeclaration,
     typeChecker: ts.TypeChecker,
     options: PluginOptions,
     hostFilename: string,
     sourceFile: ts.SourceFile
   ): ts.MethodDeclaration {
-    const node = ts.getMutableClone(compilerNode);
-    if (!node.decorators) {
-      return node;
+    if (!compilerNode.decorators) {
+      return compilerNode;
     }
-    const nodeArray = node.decorators;
-    const { pos, end } = nodeArray;
 
-    (node as any).decorators = Object.assign(
+    return factory.updateMethodDeclaration(
+      compilerNode,
       [
         ...this.createApiOperationDecorator(
-          node,
-          nodeArray,
+          factory,
+          compilerNode,
+          compilerNode.decorators,
           options,
           sourceFile,
           typeChecker
         ),
-        ...nodeArray,
-        ts.createDecorator(
-          ts.createCall(
-            ts.createIdentifier(`${OPENAPI_NAMESPACE}.${ApiResponse.name}`),
+        ...compilerNode.decorators,
+        factory.createDecorator(
+          factory.createCallExpression(
+            factory.createIdentifier(
+              `${OPENAPI_NAMESPACE}.${ApiResponse.name}`
+            ),
             undefined,
             [
               this.createDecoratorObjectLiteralExpr(
-                node,
+                factory,
+                compilerNode,
                 typeChecker,
-                ts.createNodeArray(),
+                factory.createNodeArray(),
                 hostFilename
               )
             ]
           )
         )
       ],
-      { pos, end }
+      compilerNode.modifiers,
+      compilerNode.asteriskToken,
+      compilerNode.name,
+      compilerNode.questionToken,
+      compilerNode.typeParameters,
+      compilerNode.parameters,
+      compilerNode.type,
+      compilerNode.body
     );
-    return node;
   }
 
   createApiOperationDecorator(
+    factory: ts.NodeFactory,
     node: ts.MethodDeclaration,
     nodeArray: ts.NodeArray<ts.Decorator>,
     options: PluginOptions,
@@ -126,23 +137,27 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
         return [];
       }
       const properties = [
-        ts.createPropertyAssignment(
+        factory.createPropertyAssignment(
           keyToGenerate,
-          ts.createLiteral(extractedComments)
+          factory.createStringLiteral(extractedComments)
         ),
-        ...(apiOperationExprProperties ?? ts.createNodeArray())
+        ...(apiOperationExprProperties ?? factory.createNodeArray())
       ];
       const apiOperationDecoratorArguments: ts.NodeArray<ts.Expression> =
-        ts.createNodeArray([ts.createObjectLiteral(compact(properties))]);
+        factory.createNodeArray([
+          factory.createObjectLiteralExpression(compact(properties))
+        ]);
       if (apiOperationDecorator) {
         (
           apiOperationDecorator.expression as ts.CallExpression as any
         ).arguments = apiOperationDecoratorArguments;
       } else {
         return [
-          ts.createDecorator(
-            ts.createCall(
-              ts.createIdentifier(`${OPENAPI_NAMESPACE}.${ApiOperation.name}`),
+          factory.createDecorator(
+            factory.createCallExpression(
+              factory.createIdentifier(
+                `${OPENAPI_NAMESPACE}.${ApiOperation.name}`
+              ),
               undefined,
               apiOperationDecoratorArguments
             )
@@ -154,25 +169,28 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
   }
 
   createDecoratorObjectLiteralExpr(
+    factory: ts.NodeFactory,
     node: ts.MethodDeclaration,
     typeChecker: ts.TypeChecker,
-    existingProperties: ts.NodeArray<ts.PropertyAssignment> = ts.createNodeArray(),
+    existingProperties: ts.NodeArray<ts.PropertyAssignment> = factory.createNodeArray(),
     hostFilename: string
   ): ts.ObjectLiteralExpression {
     const properties = [
       ...existingProperties,
-      this.createStatusPropertyAssignment(node, existingProperties),
+      this.createStatusPropertyAssignment(factory, node, existingProperties),
       this.createTypePropertyAssignment(
+        factory,
         node,
         typeChecker,
         existingProperties,
         hostFilename
       )
     ];
-    return ts.createObjectLiteral(compact(properties));
+    return factory.createObjectLiteralExpression(compact(properties));
   }
 
   createTypePropertyAssignment(
+    factory: ts.NodeFactory,
     node: ts.MethodDeclaration,
     typeChecker: ts.TypeChecker,
     existingProperties: ts.NodeArray<ts.PropertyAssignment>,
@@ -194,24 +212,25 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
       return undefined;
     }
     typeReference = replaceImportPath(typeReference, hostFilename);
-    return ts.createPropertyAssignment(
+    return factory.createPropertyAssignment(
       'type',
-      ts.createIdentifier(typeReference)
+      factory.createIdentifier(typeReference)
     );
   }
 
   createStatusPropertyAssignment(
+    factory: ts.NodeFactory,
     node: ts.MethodDeclaration,
     existingProperties: ts.NodeArray<ts.PropertyAssignment>
   ) {
     if (hasPropertyKey('status', existingProperties)) {
       return undefined;
     }
-    const statusNode = this.getStatusCodeIdentifier(node);
-    return ts.createPropertyAssignment('status', statusNode);
+    const statusNode = this.getStatusCodeIdentifier(factory, node);
+    return factory.createPropertyAssignment('status', statusNode);
   }
 
-  getStatusCodeIdentifier(node: ts.MethodDeclaration) {
+  getStatusCodeIdentifier(factory: ts.NodeFactory, node: ts.MethodDeclaration) {
     const decorators = node.decorators;
     const httpCodeDecorator = getDecoratorOrUndefinedByNames(
       ['HttpCode'],
@@ -225,8 +244,8 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
     }
     const postDecorator = getDecoratorOrUndefinedByNames(['Post'], decorators);
     if (postDecorator) {
-      return ts.createIdentifier('201');
+      return factory.createIdentifier('201');
     }
-    return ts.createIdentifier('200');
+    return factory.createIdentifier('200');
   }
 }
diff --git a/lib/plugin/visitors/model-class.visitor.ts b/lib/plugin/visitors/model-class.visitor.ts
index 9b104ba0..eb4517bf 100644
--- a/lib/plugin/visitors/model-class.visitor.ts
+++ b/lib/plugin/visitors/model-class.visitor.ts
@@ -4,6 +4,8 @@ import { ApiHideProperty } from '../../decorators';
 import { PluginOptions } from '../merge-options';
 import { METADATA_FACTORY_NAME } from '../plugin-constants';
 import {
+  createBooleanLiteral,
+  createPrimitiveLiteral,
   getDecoratorArguments,
   getMainCommentAndExamplesOfNode,
   getText,
@@ -54,6 +56,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
           }
           try {
             this.inspectPropertyDeclaration(
+              ctx.factory,
               node,
               typeChecker,
               options,
@@ -76,43 +79,55 @@ export class ModelClassVisitor extends AbstractFileVisitor {
           propertyNodeVisitorFactory(metadata),
           ctx
         );
-        return this.addMetadataFactory(node as ts.ClassDeclaration, metadata);
+        return this.addMetadataFactory(
+          ctx.factory,
+          node as ts.ClassDeclaration,
+          metadata
+        );
       }
       return ts.visitEachChild(node, visitClassNode, ctx);
     };
     return ts.visitNode(sourceFile, visitClassNode);
   }
 
-  addMetadataFactory(node: ts.ClassDeclaration, classMetadata: ClassMetadata) {
-    const classMutableNode = ts.getMutableClone(node);
-    const returnValue = ts.createObjectLiteral(
+  addMetadataFactory(
+    factory: ts.NodeFactory,
+    node: ts.ClassDeclaration,
+    classMetadata: ClassMetadata
+  ) {
+    const returnValue = factory.createObjectLiteralExpression(
       Object.keys(classMetadata).map((key) =>
-        ts.createPropertyAssignment(
-          ts.createIdentifier(key),
+        factory.createPropertyAssignment(
+          factory.createIdentifier(key),
           classMetadata[key]
         )
       )
     );
-    const method = ts.createMethod(
+    const method = factory.createMethodDeclaration(
       undefined,
-      [ts.createModifier(ts.SyntaxKind.StaticKeyword)],
+      [factory.createModifier(ts.SyntaxKind.StaticKeyword)],
       undefined,
-      ts.createIdentifier(METADATA_FACTORY_NAME),
+      factory.createIdentifier(METADATA_FACTORY_NAME),
       undefined,
       undefined,
       [],
       undefined,
-      ts.createBlock([ts.createReturn(returnValue)], true)
+      factory.createBlock([factory.createReturnStatement(returnValue)], true)
+    );
+
+    return factory.updateClassDeclaration(
+      node,
+      node.decorators,
+      node.modifiers,
+      node.name,
+      node.typeParameters,
+      node.heritageClauses,
+      [...node.members, method]
     );
-    (classMutableNode as ts.ClassDeclaration as any).members =
-      ts.createNodeArray([
-        ...(classMutableNode as ts.ClassDeclaration).members,
-        method
-      ]);
-    return classMutableNode;
   }
 
   inspectPropertyDeclaration(
+    factory: ts.NodeFactory,
     compilerNode: ts.PropertyDeclaration,
     typeChecker: ts.TypeChecker,
     options: PluginOptions,
@@ -121,9 +136,10 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     metadata: ClassMetadata
   ) {
     const objectLiteralExpr = this.createDecoratorObjectLiteralExpr(
+      factory,
       compilerNode,
       typeChecker,
-      ts.createNodeArray(),
+      factory.createNodeArray(),
       options,
       hostFilename,
       sourceFile
@@ -137,9 +153,10 @@ export class ModelClassVisitor extends AbstractFileVisitor {
   }
 
   createDecoratorObjectLiteralExpr(
+    factory: ts.NodeFactory,
     node: ts.PropertyDeclaration | ts.PropertySignature,
     typeChecker: ts.TypeChecker,
-    existingProperties: ts.NodeArray<ts.PropertyAssignment> = ts.createNodeArray(),
+    existingProperties: ts.NodeArray<ts.PropertyAssignment> = factory.createNodeArray(),
     options: PluginOptions = {},
     hostFilename = '',
     sourceFile?: ts.SourceFile
@@ -149,22 +166,28 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     let properties = [
       ...existingProperties,
       !hasPropertyKey('required', existingProperties) &&
-        ts.createPropertyAssignment('required', ts.createLiteral(isRequired)),
+        factory.createPropertyAssignment(
+          'required',
+          createBooleanLiteral(factory, isRequired)
+        ),
       ...this.createTypePropertyAssignments(
+        factory,
         node.type,
         typeChecker,
         existingProperties,
         hostFilename
       ),
       ...this.createDescriptionAndExamplePropertyAssigments(
+        factory,
         node,
         typeChecker,
         existingProperties,
         options,
         sourceFile
       ),
-      this.createDefaultPropertyAssignment(node, existingProperties),
+      this.createDefaultPropertyAssignment(factory, node, existingProperties),
       this.createEnumPropertyAssignment(
+        factory,
         node,
         typeChecker,
         existingProperties,
@@ -173,11 +196,10 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     ];
     if (options.classValidatorShim) {
       properties = properties.concat(
-        this.createValidationPropertyAssignments(node)
+        this.createValidationPropertyAssignments(factory, node)
       );
     }
-    const objectLiteral = ts.createObjectLiteral(compact(flatten(properties)));
-    return objectLiteral;
+    return factory.createObjectLiteralExpression(compact(flatten(properties)));
   }
 
   /**
@@ -187,6 +209,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
    * - 'nullable'
    */
   private createTypePropertyAssignments(
+    factory: ts.NodeFactory,
     node: ts.TypeNode,
     typeChecker: ts.TypeChecker,
     existingProperties: ts.NodeArray<ts.PropertyAssignment>,
@@ -201,28 +224,31 @@ export class ModelClassVisitor extends AbstractFileVisitor {
         const propertyAssignments = Array.from(node.members || []).map(
           (member) => {
             const literalExpr = this.createDecoratorObjectLiteralExpr(
+              factory,
               member as ts.PropertySignature,
               typeChecker,
               existingProperties,
               {},
               hostFilename
             );
-            return ts.createPropertyAssignment(
-              ts.createIdentifier(member.name.getText()),
+            return factory.createPropertyAssignment(
+              factory.createIdentifier(member.name.getText()),
               literalExpr
             );
           }
         );
         return [
-          ts.createPropertyAssignment(
+          factory.createPropertyAssignment(
             key,
-            ts.createArrowFunction(
+            factory.createArrowFunction(
               undefined,
               undefined,
               [],
               undefined,
               undefined,
-              ts.createParen(ts.createObjectLiteral(propertyAssignments))
+              factory.createParenthesizedExpression(
+                factory.createObjectLiteralExpression(propertyAssignments)
+              )
             )
           )
         ];
@@ -240,6 +266,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
         // When we have more than 1 type left, we could use oneOf
         if (remainingTypes.length === 1) {
           const remainingTypesProperties = this.createTypePropertyAssignments(
+            factory,
             remainingTypes[0],
             typeChecker,
             existingProperties,
@@ -250,9 +277,9 @@ export class ModelClassVisitor extends AbstractFileVisitor {
             ...remainingTypesProperties
           );
           if (isNullable) {
-            const nullablePropertyAssignment = ts.createPropertyAssignment(
+            const nullablePropertyAssignment = factory.createPropertyAssignment(
               'nullable',
-              ts.createTrue()
+              createBooleanLiteral(factory, true)
             );
             resultArray.push(nullablePropertyAssignment);
           }
@@ -271,21 +298,22 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     }
     typeReference = replaceImportPath(typeReference, hostFilename);
     return [
-      ts.createPropertyAssignment(
+      factory.createPropertyAssignment(
         key,
-        ts.createArrowFunction(
+        factory.createArrowFunction(
           undefined,
           undefined,
           [],
           undefined,
           undefined,
-          ts.createIdentifier(typeReference)
+          factory.createIdentifier(typeReference)
         )
       )
     ];
   }
 
   createEnumPropertyAssignment(
+    factory: ts.NodeFactory,
     node: ts.PropertyDeclaration | ts.PropertySignature,
     typeChecker: ts.TypeChecker,
     existingProperties: ts.NodeArray<ts.PropertyAssignment>,
@@ -327,15 +355,15 @@ export class ModelClassVisitor extends AbstractFileVisitor {
       type = typeIsArrayTuple.type;
     }
     const enumRef = replaceImportPath(getText(type, typeChecker), hostFilename);
-    const enumProperty = ts.createPropertyAssignment(
+    const enumProperty = factory.createPropertyAssignment(
       key,
-      ts.createIdentifier(enumRef)
+      factory.createIdentifier(enumRef)
     );
     if (isArrayType) {
       const isArrayKey = 'isArray';
-      const isArrayProperty = ts.createPropertyAssignment(
+      const isArrayProperty = factory.createPropertyAssignment(
         isArrayKey,
-        ts.createIdentifier('true')
+        factory.createIdentifier('true')
       );
       return [enumProperty, isArrayProperty];
     }
@@ -343,6 +371,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
   }
 
   createDefaultPropertyAssignment(
+    factory: ts.NodeFactory,
     node: ts.PropertyDeclaration | ts.PropertySignature,
     existingProperties: ts.NodeArray<ts.PropertyAssignment>
   ) {
@@ -357,34 +386,39 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     if (ts.isAsExpression(initializer)) {
       initializer = initializer.expression;
     }
-    return ts.createPropertyAssignment(key, ts.getMutableClone(initializer));
+    return factory.createPropertyAssignment(key, initializer);
   }
 
   createValidationPropertyAssignments(
+    factory: ts.NodeFactory,
     node: ts.PropertyDeclaration | ts.PropertySignature
   ): ts.PropertyAssignment[] {
     const assignments = [];
     const decorators = node.decorators;
 
     this.addPropertyByValidationDecorator(
+      factory,
       'Min',
       'minimum',
       decorators,
       assignments
     );
     this.addPropertyByValidationDecorator(
+      factory,
       'Max',
       'maximum',
       decorators,
       assignments
     );
     this.addPropertyByValidationDecorator(
+      factory,
       'MinLength',
       'minLength',
       decorators,
       assignments
     );
     this.addPropertyByValidationDecorator(
+      factory,
       'MaxLength',
       'maxLength',
       decorators,
@@ -395,6 +429,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
   }
 
   addPropertyByValidationDecorator(
+    factory: ts.NodeFactory,
     decoratorName: string,
     propertyKey: string,
     decorators: ts.NodeArray<ts.Decorator>,
@@ -409,9 +444,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     }
     const argument: ts.Expression = head(getDecoratorArguments(decoratorRef));
     if (argument) {
-      assignments.push(
-        ts.createPropertyAssignment(propertyKey, ts.getMutableClone(argument))
-      );
+      assignments.push(factory.createPropertyAssignment(propertyKey, argument));
     }
   }
 
@@ -437,9 +470,10 @@ export class ModelClassVisitor extends AbstractFileVisitor {
   }
 
   createDescriptionAndExamplePropertyAssigments(
+    factory: ts.NodeFactory,
     node: ts.PropertyDeclaration | ts.PropertySignature,
     typeChecker: ts.TypeChecker,
-    existingProperties: ts.NodeArray<ts.PropertyAssignment> = ts.createNodeArray(),
+    existingProperties: ts.NodeArray<ts.PropertyAssignment> = factory.createNodeArray(),
     options: PluginOptions = {},
     sourceFile?: ts.SourceFile
   ): ts.PropertyAssignment[] {
@@ -456,9 +490,9 @@ export class ModelClassVisitor extends AbstractFileVisitor {
 
     const keyOfComment = options.dtoKeyOfComment;
     if (!hasPropertyKey(keyOfComment, existingProperties) && comments) {
-      const descriptionPropertyAssignment = ts.createPropertyAssignment(
+      const descriptionPropertyAssignment = factory.createPropertyAssignment(
         keyOfComment,
-        ts.createLiteral(comments)
+        factory.createStringLiteral(comments)
       );
       propertyAssignments.push(descriptionPropertyAssignment);
     }
@@ -469,15 +503,15 @@ export class ModelClassVisitor extends AbstractFileVisitor {
 
     if (!hasExampleOrExamplesKey && examples.length) {
       if (examples.length === 1) {
-        const examplePropertyAssignment = ts.createPropertyAssignment(
+        const examplePropertyAssignment = factory.createPropertyAssignment(
           'example',
-          this.createLiteralFromAnyValue(examples[0])
+          this.createLiteralFromAnyValue(factory, examples[0])
         );
         propertyAssignments.push(examplePropertyAssignment);
       } else {
-        const examplesPropertyAssignment = ts.createPropertyAssignment(
+        const examplesPropertyAssignment = factory.createPropertyAssignment(
           'examples',
-          this.createLiteralFromAnyValue(examples)
+          this.createLiteralFromAnyValue(factory, examples)
         );
         propertyAssignments.push(examplesPropertyAssignment);
       }
@@ -485,11 +519,11 @@ export class ModelClassVisitor extends AbstractFileVisitor {
     return propertyAssignments;
   }
 
-  private createLiteralFromAnyValue(item: any) {
+  private createLiteralFromAnyValue(factory: ts.NodeFactory, item: unknown) {
     return Array.isArray(item)
-      ? ts.createArrayLiteral(
-          item.map((item) => this.createLiteralFromAnyValue(item))
+      ? factory.createArrayLiteralExpression(
+          item.map((item) => this.createLiteralFromAnyValue(factory, item))
         )
-      : ts.createLiteral(item);
+      : createPrimitiveLiteral(factory, item);
   }
 }
diff --git a/lib/services/schema-object-factory.ts b/lib/services/schema-object-factory.ts
index 32f83866..45d0a4ce 100644
--- a/lib/services/schema-object-factory.ts
+++ b/lib/services/schema-object-factory.ts
@@ -226,69 +226,17 @@ export class SchemaObjectFactory {
         metadata.isArray
       );
     }
-    if (this.isObjectLiteral(metadata.type as Record<string, any>)) {
-      return this.createFromObjectLiteral(
-        key,
-        metadata.type as Record<string, any>,
-        schemas
-      );
-    }
-    if (isString(metadata.type)) {
-      if (isEnumMetadata(metadata)) {
-        return this.createEnumSchemaType(key, metadata, schemas);
-      }
-      if (metadata.isArray) {
-        return this.transformToArraySchemaProperty(
-          metadata,
-          key,
-          metadata.type
-        );
-      }
 
-      return {
-        ...metadata,
-        name: metadata.name || key
-      };
-    }
-    if (isDateCtor(metadata.type as Function)) {
-      if (metadata.isArray) {
-        return this.transformToArraySchemaProperty(metadata, key, {
-          format: metadata.format || 'date-time',
-          type: 'string'
-        });
-      }
-      return {
-        format: 'date-time',
-        ...metadata,
-        type: 'string',
-        name: metadata.name || key
-      };
-    }
-    if (!isBuiltInType(metadata.type as Function)) {
-      return this.createNotBuiltInTypeReference(
+    if (Array.isArray(metadata.type)) {
+      return this.createFromNestedArray(
         key,
         metadata,
         schemas,
         pendingSchemaRefs
       );
     }
-    const typeName = this.getTypeName(metadata.type as Type<unknown>);
-    const itemType = this.swaggerTypesMapper.mapTypeToOpenAPIType(typeName);
-    if (metadata.isArray) {
-      return this.transformToArraySchemaProperty(metadata, key, {
-        type: itemType
-      });
-    } else if (itemType === 'array') {
-      const defaultOnArray = 'string';
-      return this.transformToArraySchemaProperty(metadata, key, {
-        type: defaultOnArray
-      });
-    }
-    return {
-      ...metadata,
-      name: metadata.name || key,
-      type: itemType
-    };
+
+    return this.createSchemaMetadata(key, metadata, schemas, pendingSchemaRefs);
   }
 
   createEnumParam(
@@ -363,22 +311,23 @@ export class SchemaObjectFactory {
   createNotBuiltInTypeReference(
     key: string,
     metadata: SchemaObjectMetadata,
+    trueMetadataType: unknown,
     schemas: Record<string, SchemaObject>,
     pendingSchemaRefs: string[]
   ): SchemaObjectMetadata {
-    if (isUndefined(metadata.type)) {
+    if (isUndefined(trueMetadataType)) {
       throw new Error(
         `A circular dependency has been detected (property key: "${key}"). Please, make sure that each side of a bidirectional relationships are using lazy resolvers ("type: () => ClassType").`
       );
     }
-    let schemaObjectName = (metadata.type as Function).name;
+    let schemaObjectName = (trueMetadataType as Function).name;
 
     if (
       !(schemaObjectName in schemas) &&
       !pendingSchemaRefs.includes(schemaObjectName)
     ) {
       schemaObjectName = this.exploreModelSchema(
-        metadata.type as Function,
+        trueMetadataType as Function,
         schemas,
         [...pendingSchemaRefs, schemaObjectName]
       );
@@ -412,15 +361,8 @@ 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 [movedProperties, keysToMove] =
+      this.extractPropertyModifiers(metadata);
     const schemaHost = {
       ...omit(metadata, [...keysToRemove, ...keysToMove]),
       name: metadata.name || key,
@@ -430,6 +372,7 @@ export class SchemaObjectFactory {
         : { ...type, ...movedProperties }
     };
     schemaHost.items = omitBy(schemaHost.items, isUndefined);
+
     return schemaHost as unknown;
   }
 
@@ -487,6 +430,104 @@ export class SchemaObjectFactory {
     };
   }
 
+  createFromNestedArray(
+    key: string,
+    metadata: SchemaObjectMetadata,
+    schemas: Record<string, SchemaObject>,
+    pendingSchemaRefs: string[]
+  ) {
+    const recurse = (type: unknown) => {
+      if (!Array.isArray(type)) {
+        const schemaMetadata = this.createSchemaMetadata(
+          key,
+          metadata,
+          schemas,
+          pendingSchemaRefs,
+          type
+        );
+        return omit(schemaMetadata, ['isArray', 'name']);
+      }
+
+      return {
+        name: key,
+        type: 'array',
+        items: recurse(type[0])
+      };
+    };
+
+    return recurse(metadata.type);
+  }
+
+  private createSchemaMetadata(
+    key: string,
+    metadata: SchemaObjectMetadata,
+    schemas: Record<string, SchemaObject>,
+    pendingSchemaRefs: string[],
+    nestedArrayType?: unknown
+  ) {
+    const trueType = nestedArrayType || metadata.type;
+    if (this.isObjectLiteral(trueType as Record<string, any>)) {
+      return this.createFromObjectLiteral(
+        key,
+        trueType as Record<string, any>,
+        schemas
+      );
+    }
+    if (isString(trueType)) {
+      if (isEnumMetadata(metadata)) {
+        return this.createEnumSchemaType(key, metadata, schemas);
+      }
+      if (metadata.isArray) {
+        return this.transformToArraySchemaProperty(metadata, key, trueType);
+      }
+
+      return {
+        ...metadata,
+        name: metadata.name || key
+      };
+    }
+    if (isDateCtor(trueType as Function)) {
+      if (metadata.isArray) {
+        return this.transformToArraySchemaProperty(metadata, key, {
+          format: metadata.format || 'date-time',
+          type: 'string'
+        });
+      }
+      return {
+        format: 'date-time',
+        ...metadata,
+        type: 'string',
+        name: metadata.name || key
+      };
+    }
+    if (!isBuiltInType(trueType as Function)) {
+      return this.createNotBuiltInTypeReference(
+        key,
+        metadata,
+        trueType,
+        schemas,
+        pendingSchemaRefs
+      );
+    }
+    const typeName = this.getTypeName(trueType as Type<unknown>);
+    const itemType = this.swaggerTypesMapper.mapTypeToOpenAPIType(typeName);
+    if (metadata.isArray) {
+      return this.transformToArraySchemaProperty(metadata, key, {
+        type: itemType
+      });
+    } else if (itemType === 'array') {
+      const defaultOnArray = 'string';
+      return this.transformToArraySchemaProperty(metadata, key, {
+        type: defaultOnArray
+      });
+    }
+    return {
+      ...metadata,
+      name: metadata.name || key,
+      type: itemType
+    };
+  }
+
   private isArrayCtor(type: Type<unknown> | string): boolean {
     return type === Array;
   }
@@ -527,4 +568,18 @@ export class SchemaObjectFactory {
     }
     return Object.getPrototypeOf(obj) === objPrototype;
   }
+
+  private extractPropertyModifiers(
+    metadata: SchemaObjectMetadata
+  ): [Partial<SchemaObjectMetadata>, string[]] {
+    const modifierKeys = [
+      'format',
+      'maximum',
+      'maxLength',
+      'minimum',
+      'minLength',
+      'pattern'
+    ];
+    return [pick(metadata, modifierKeys), modifierKeys];
+  }
 }


Test patch:
diff --git a/test/plugin/fixtures/create-cat.dto.ts b/test/plugin/fixtures/create-cat.dto.ts
index 140c591f..248d5d79 100644
--- a/test/plugin/fixtures/create-cat.dto.ts
+++ b/test/plugin/fixtures/create-cat.dto.ts
@@ -14,6 +14,10 @@ interface Node {
     id: number;
 }
 
+class OtherNode {
+    id: number;
+}
+
 export class CreateCatDto {
   name: string;
   @Min(0)
@@ -34,6 +38,9 @@ export class CreateCatDto {
   nodes: Node[];
   optionalBoolean?: boolean;
   date: Date;
+  
+  twoDimensionPrimitives: string[][];
+  twoDimensionNodes: OtherNode[][];
 
   @ApiHideProperty()
   hidden: number;
@@ -52,13 +59,18 @@ var OneValueEnum;
 (function (OneValueEnum) {
     OneValueEnum[OneValueEnum[\"ONE\"] = 0] = \"ONE\";
 })(OneValueEnum || (OneValueEnum = {}));
+class OtherNode {
+    static _OPENAPI_METADATA_FACTORY() {
+        return { id: { required: true, type: () => Number } };
+    }
+}
 export class CreateCatDto {
     constructor() {
         this.age = 3;
         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, title: "this is breed im comment" }, 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: { required: false, type: () => String, title: "this is breed im comment" }, nodes: { required: true, type: () => [Object] }, optionalBoolean: { required: false, type: () => Boolean }, date: { required: true, type: () => Date }, twoDimensionPrimitives: { required: true, type: () => [[String]] }, twoDimensionNodes: { required: true, type: () => [[OtherNode]] } };
     }
 }
 __decorate([
diff --git a/test/services/fixtures/create-user.dto.ts b/test/services/fixtures/create-user.dto.ts
index 8c9f5fa8..38a45a8a 100644
--- a/test/services/fixtures/create-user.dto.ts
+++ b/test/services/fixtures/create-user.dto.ts
@@ -44,6 +44,14 @@ export class CreateUserDto {
   @ApiProperty()
   tags: string[];
 
+  @ApiProperty()
+  twoDimensionPrimitives: string[][];
+
+  @ApiProperty({
+    type: () => [[CreateProfileDto]]
+  })
+  twoDimensionModels: CreateProfileDto[][];
+
   @ApiProperty({
     type: String,
     isArray: true,
@@ -87,7 +95,8 @@ export class CreateUserDto {
 
   static _OPENAPI_METADATA_FACTORY() {
     return {
-      tags: { type: () => [String] }
+      tags: { type: () => [String] },
+      twoDimensionPrimitives: { type: () => [[String]] }
     };
   }
 }
diff --git a/test/services/schema-object-factory.spec.ts b/test/services/schema-object-factory.spec.ts
index f10ca163..3df7f5c5 100644
--- a/test/services/schema-object-factory.spec.ts
+++ b/test/services/schema-object-factory.spec.ts
@@ -144,6 +144,24 @@ describe('SchemaObjectFactory', () => {
             },
             type: 'array'
           },
+          twoDimensionPrimitives: {
+            items: {
+              type: 'array',
+              items: {
+                type: 'string'
+              }
+            },
+            type: 'array'
+          },
+          twoDimensionModels: {
+            items: {
+              type: 'array',
+              items: {
+                $ref: '#/components/schemas/CreateProfileDto'
+              }
+            },
+            type: 'array'
+          },
           urls: {
             items: {
               format: 'uri',
@@ -181,6 +199,8 @@ describe('SchemaObjectFactory', () => {
           'password',
           'profile',
           'tags',
+          'twoDimensionPrimitives',
+          'twoDimensionModels',
           'urls',
           'luckyNumbers',
           'options',