Repository: lukeautry/tsoa
Pull request #71 (2017-03-05 05:24:47)

Added genericType handling for request/response objects
This PR implements this feature request https://github.com/lukeautry/tsoa/issues/27.

It adds the ability to return and take in generic types.


Example:

```javascript
@Route('user')
export class UserController {

    @Get('{id}')
    public async getUser(@Path() id: string): Promise<IResponse<User>> {
        return {
            success: true,
            result: new User(id, 'Foo Bar')
        };
    }
}

export interface IResponse<T> {
    success: boolean;
    result: T;
}

export class User {
    public id: string;
    public name: string;

    constructor(id, name) {
        this.id = id;
        this.name = name;
    }
}
```


Patch:
diff --git a/src/metadataGeneration/resolveType.ts b/src/metadataGeneration/resolveType.ts
index d6bac76a..05cf4441 100644
--- a/src/metadataGeneration/resolveType.ts
+++ b/src/metadataGeneration/resolveType.ts
@@ -1,5 +1,6 @@
 import * as ts from 'typescript';
 import { MetadataGenerator, Type, ReferenceType, Property } from './metadataGenerator';
+import * as _ from 'lodash';
 
 const syntaxKindMap: { [kind: number]: string } = {};
 syntaxKindMap[ts.SyntaxKind.NumberKeyword] = 'number';
@@ -42,29 +43,41 @@ export function ResolveType(typeNode: ts.TypeNode): Type {
     }
   }
 
-  const referenceType = generateReferenceType(typeReference.typeName as ts.EntityName);
+  let referenceType: ReferenceType;
+
+  if (typeReference.typeArguments && typeReference.typeArguments.length === 1) {
+    let typeT: ts.TypeNode[] = typeReference.typeArguments as ts.TypeNode[];
+    referenceType = generateReferenceType(typeReference.typeName, typeT);
+  } else {
+    referenceType = generateReferenceType(typeReference.typeName);
+  }
+
   MetadataGenerator.current.AddReferenceType(referenceType);
   return referenceType;
 }
 
-function generateReferenceType(type: ts.EntityName): ReferenceType {
+function generateReferenceType(type: ts.EntityName, genericTypes?: ts.TypeNode[]): ReferenceType {
   const typeName = resolveFqTypeName(type);
+  const typeNameWithGenerics = getTypeName(typeName, genericTypes);
+
   try {
-    const existingType = localReferenceTypeCache[typeName];
+
+    const existingType = localReferenceTypeCache[typeNameWithGenerics];
     if (existingType) { return existingType; }
 
-    if (inProgressTypes[typeName]) {
-      return createCircularDependencyResolver(typeName);
+    if (inProgressTypes[typeNameWithGenerics]) {
+      return createCircularDependencyResolver(typeNameWithGenerics);
     }
 
-    inProgressTypes[typeName] = true;
+    inProgressTypes[typeNameWithGenerics] = true;
 
     const modelTypeDeclaration = getModelTypeDeclaration(type);
-    const properties = getModelTypeProperties(modelTypeDeclaration);
+
+    const properties = getModelTypeProperties(modelTypeDeclaration, genericTypes);
 
     const referenceType: ReferenceType = {
       description: getModelDescription(modelTypeDeclaration),
-      name: typeName,
+      name: typeNameWithGenerics,
       properties: properties
     };
     if (modelTypeDeclaration.kind === ts.SyntaxKind.TypeAliasDeclaration) {
@@ -78,10 +91,11 @@ function generateReferenceType(type: ts.EntityName): ReferenceType {
     const extendedProperties = getInheritedProperties(modelTypeDeclaration);
     referenceType.properties = referenceType.properties.concat(extendedProperties);
 
-    localReferenceTypeCache[typeName] = referenceType;
+    localReferenceTypeCache[typeNameWithGenerics] = referenceType;
+
     return referenceType;
   } catch (err) {
-    console.error(`There was a problem resolving type of '${typeName}'.`);
+    console.error(`There was a problem resolving type of '${getTypeName(typeName, genericTypes)}'.`);
     throw err;
   }
 }
@@ -95,6 +109,47 @@ function resolveFqTypeName(type: ts.EntityName): string {
   return resolveFqTypeName(qualifiedType.left) + '.' + (qualifiedType.right as ts.Identifier).text;
 }
 
+function getTypeName(typeName: string, genericTypes?: ts.TypeNode[]): string {
+  if (!genericTypes || !genericTypes.length) {
+    return typeName;
+  }
+  let names = genericTypes.map((t) => {
+    return getAnyTypeName(t);
+  });
+
+  return typeName + names.join('');
+}
+
+function getAnyTypeName(typeNode: ts.TypeNode): string {
+  const primitiveType = syntaxKindMap[typeNode.kind];
+  if (primitiveType) {
+    return primitiveType;
+  }
+
+  if (typeNode.kind === ts.SyntaxKind.ArrayType) {
+    const arrayType = typeNode as ts.ArrayTypeNode;
+    return getAnyTypeName(arrayType.elementType) + '[]';
+  }
+
+  if (typeNode.kind === ts.SyntaxKind.UnionType) {
+    return 'object';
+  }
+
+  if (typeNode.kind !== ts.SyntaxKind.TypeReference) {
+    throw new Error(`Unknown type: ${ts.SyntaxKind[typeNode.kind]}`);
+  }
+
+  const typeReference = typeNode as ts.TypeReferenceNode;
+  try {
+    return (typeReference.typeName as ts.Identifier).text;
+  } catch (e) {
+    // idk what would hit this? probably needs more testing
+    console.error(e);
+    return typeNode.toString();
+  }
+
+}
+
 function createCircularDependencyResolver(typeName: string) {
   const referenceType = {
     description: '',
@@ -184,22 +239,52 @@ function getModelTypeDeclaration(type: ts.EntityName) {
   return modelTypes[0];
 }
 
-function getModelTypeProperties(node: UsableDeclaration) {
+function getModelTypeProperties(node: UsableDeclaration, genericTypes?: ts.TypeNode[]) {
   if (node.kind === ts.SyntaxKind.InterfaceDeclaration) {
     const interfaceDeclaration = node as ts.InterfaceDeclaration;
     return interfaceDeclaration.members
       .filter(member => member.kind === ts.SyntaxKind.PropertySignature)
       .map((property: any) => {
+
         const propertyDeclaration = property as ts.PropertyDeclaration;
         const identifier = propertyDeclaration.name as ts.Identifier;
 
         if (!propertyDeclaration.type) { throw new Error('No valid type found for property declaration.'); }
 
+        // Declare a variable that can be overridden if needed
+        let aType = propertyDeclaration.type;
+
+        // aType.kind will always be a TypeReference when the property of Interface<T> is of type T
+        if (aType.kind === ts.SyntaxKind.TypeReference && genericTypes && genericTypes.length && node.typeParameters) {
+
+          // The type definitions are conviently located on the object which allow us to map -> to the genericTypes
+          let typeParams = _.map(node.typeParameters, (typeParam: ts.TypeParameterDeclaration) => {
+            return typeParam.name.text;
+          });
+
+          // I am not sure in what cases
+          const typeIdentifier = (aType as ts.TypeReferenceNode).typeName;
+          let typeIdentifierName: string;
+
+          // typeIdentifier can either be a Identifier or a QualifiedName
+          if ((typeIdentifier as ts.Identifier).text) {
+            typeIdentifierName = (typeIdentifier as ts.Identifier).text;
+          } else {
+            typeIdentifierName = (typeIdentifier as ts.QualifiedName).right.text;
+          }
+
+          // I could not produce a situation where this did not find it so its possible this check is irrelevant
+          const indexOfType = _.indexOf<string>(typeParams, typeIdentifierName);
+          if (indexOfType >= 0) {
+            aType = genericTypes[indexOfType] as ts.TypeNode;
+          }
+        }
+
         return {
           description: getNodeDescription(propertyDeclaration),
           name: identifier.text,
           required: !property.questionToken,
-          type: ResolveType(propertyDeclaration.type)
+          type: ResolveType(aType)
         };
       });
   }
diff --git a/src/routeGeneration/routeGenerator.ts b/src/routeGeneration/routeGenerator.ts
index dd9a6a52..67134514 100644
--- a/src/routeGeneration/routeGenerator.ts
+++ b/src/routeGeneration/routeGenerator.ts
@@ -78,7 +78,7 @@ export class RouteGenerator {
                 },
                 {{/each}}
             };
-        `.concat(middlewareTemplate));
+        `.concat(middlewareTemplate), { noEscape: true });
 
     const authenticationModule = this.options.authenticationModule ? this.getRelativeImportPath(this.options.authenticationModule) : undefined;
     const iocModule = this.options.iocModule ? this.getRelativeImportPath(this.options.iocModule) : undefined;


Test patch:
diff --git a/tests/fixtures/controllers/getController.ts b/tests/fixtures/controllers/getController.ts
index 9a2eb9b7..df443b3c 100644
--- a/tests/fixtures/controllers/getController.ts
+++ b/tests/fixtures/controllers/getController.ts
@@ -4,7 +4,7 @@ import { Get } from '../../../src/decorators/methods';
 import { Controller } from '../../../src/interfaces/controller';
 import { ModelService } from '../services/modelService';
 import { Route } from '../../../src/decorators/route';
-import { TestModel, TestSubModel, TestClassModel } from '../testModel';
+import { GenericModel, TestModel, TestSubModel, TestClassModel } from '../testModel';
 import { Tags } from '../../../src/decorators/tags';
 
 @Route('GetTest')
@@ -122,6 +122,36 @@ export class GetTestController extends Controller {
   public async getBuffer(@Query() buffer: Buffer): Promise<Buffer> {
     return new Buffer('testbuffer');
   }
+
+  @Get('GenericModel')
+  public async getGenericModel(): Promise<GenericModel<TestModel>> {
+    return {
+      result: new ModelService().getModel()
+    };
+  }
+
+  @Get('GenericModelArray')
+  public async getGenericModelArray(): Promise<GenericModel<TestModel[]>> {
+    return {
+      result: [
+        new ModelService().getModel()
+      ]
+    };
+  }
+
+  @Get('GenericPrimitive')
+  public async getGenericPrimitive(): Promise<GenericModel<string>> {
+    return {
+      result: new ModelService().getModel().stringValue
+    };
+  }
+
+  @Get('GenericPrimitiveArray')
+  public async getGenericPrimitiveArray(): Promise<GenericModel<string[]>> {
+    return {
+      result: new ModelService().getModel().stringArray
+    };
+  }
 }
 
 export interface ErrorResponse {
diff --git a/tests/fixtures/controllers/postController.ts b/tests/fixtures/controllers/postController.ts
index c15b7d98..34a34e31 100644
--- a/tests/fixtures/controllers/postController.ts
+++ b/tests/fixtures/controllers/postController.ts
@@ -1,7 +1,7 @@
 import { Route } from '../../../src/decorators/route';
 import { Body, Query } from '../../../src/decorators/parameter';
 import { Post, Patch } from '../../../src/decorators/methods';
-import { TestModel, TestClassModel } from '../testModel';
+import { GenericRequest, TestModel, TestClassModel } from '../testModel';
 import { ModelService } from '../services/modelService';
 
 @Route('PostTest')
@@ -48,4 +48,9 @@ export class PostTestController {
   public async postWithBodyAndQueryParams(@Body() model: TestModel, @Query() query: string): Promise<TestModel> {
     return new ModelService().getModel();
   }
+
+  @Post('GenericBody')
+  public async getGenericRequest(@Body() genericReq: GenericRequest<TestModel>): Promise<TestModel> {
+    return genericReq.value;
+  }
 }
diff --git a/tests/fixtures/express/routes.ts b/tests/fixtures/express/routes.ts
index e10fe90c..43c40372 100644
--- a/tests/fixtures/express/routes.ts
+++ b/tests/fixtures/express/routes.ts
@@ -65,9 +65,25 @@ const models: any = {
     'optionalPublicConstructorVar': { typeName: 'string', required: false },
     'id': { typeName: 'number', required: true },
   },
+  'GenericRequestTestModel': {
+    'name': { typeName: 'string', required: true },
+    'value': { typeName: 'TestModel', required: true },
+  },
   'Result': {
     'value': { typeName: 'object', required: true },
   },
+  'GenericModelTestModel': {
+    'result': { typeName: 'TestModel', required: true },
+  },
+  'GenericModelTestModel[]': {
+    'result': { typeName: 'array', required: true, arrayType: 'TestModel' },
+  },
+  'GenericModelstring': {
+    'result': { typeName: 'string', required: true },
+  },
+  'GenericModelstring[]': {
+    'result': { typeName: 'array', required: true, arrayType: 'string' },
+  },
   'ErrorResponseModel': {
     'status': { typeName: 'number', required: true },
     'message': { typeName: 'string', required: true },
@@ -339,6 +355,29 @@ export function RegisterRoutes(app: any) {
       }
       promiseHandler(promise, statusCode, response, next);
     });
+  app.post('/v1/PostTest/GenericBody',
+    function(request: any, response: any, next: any) {
+      const args = {
+        'genericReq': { name: 'genericReq', typeName: 'GenericRequestTestModel', required: true, in: 'body', },
+      };
+
+      let validatedArgs: any[] = [];
+      try {
+        validatedArgs = getValidatedArgs(args, request);
+      } catch (err) {
+        return next(err);
+      }
+
+      const controller = new PostTestController();
+
+
+      const promise = controller.getGenericRequest.apply(controller, validatedArgs);
+      let statusCode = undefined;
+      if (controller instanceof Controller) {
+        statusCode = (controller as Controller).getStatus();
+      }
+      promiseHandler(promise, statusCode, response, next);
+    });
   app.patch('/v1/PatchTest',
     function(request: any, response: any, next: any) {
       const args = {
@@ -703,6 +742,94 @@ export function RegisterRoutes(app: any) {
       }
       promiseHandler(promise, statusCode, response, next);
     });
+  app.get('/v1/GetTest/GenericModel',
+    function(request: any, response: any, next: any) {
+      const args = {
+      };
+
+      let validatedArgs: any[] = [];
+      try {
+        validatedArgs = getValidatedArgs(args, request);
+      } catch (err) {
+        return next(err);
+      }
+
+      const controller = new GetTestController();
+
+
+      const promise = controller.getGenericModel.apply(controller, validatedArgs);
+      let statusCode = undefined;
+      if (controller instanceof Controller) {
+        statusCode = (controller as Controller).getStatus();
+      }
+      promiseHandler(promise, statusCode, response, next);
+    });
+  app.get('/v1/GetTest/GenericModelArray',
+    function(request: any, response: any, next: any) {
+      const args = {
+      };
+
+      let validatedArgs: any[] = [];
+      try {
+        validatedArgs = getValidatedArgs(args, request);
+      } catch (err) {
+        return next(err);
+      }
+
+      const controller = new GetTestController();
+
+
+      const promise = controller.getGenericModelArray.apply(controller, validatedArgs);
+      let statusCode = undefined;
+      if (controller instanceof Controller) {
+        statusCode = (controller as Controller).getStatus();
+      }
+      promiseHandler(promise, statusCode, response, next);
+    });
+  app.get('/v1/GetTest/GenericPrimitive',
+    function(request: any, response: any, next: any) {
+      const args = {
+      };
+
+      let validatedArgs: any[] = [];
+      try {
+        validatedArgs = getValidatedArgs(args, request);
+      } catch (err) {
+        return next(err);
+      }
+
+      const controller = new GetTestController();
+
+
+      const promise = controller.getGenericPrimitive.apply(controller, validatedArgs);
+      let statusCode = undefined;
+      if (controller instanceof Controller) {
+        statusCode = (controller as Controller).getStatus();
+      }
+      promiseHandler(promise, statusCode, response, next);
+    });
+  app.get('/v1/GetTest/GenericPrimitiveArray',
+    function(request: any, response: any, next: any) {
+      const args = {
+      };
+
+      let validatedArgs: any[] = [];
+      try {
+        validatedArgs = getValidatedArgs(args, request);
+      } catch (err) {
+        return next(err);
+      }
+
+      const controller = new GetTestController();
+
+
+      const promise = controller.getGenericPrimitiveArray.apply(controller, validatedArgs);
+      let statusCode = undefined;
+      if (controller instanceof Controller) {
+        statusCode = (controller as Controller).getStatus();
+      }
+      promiseHandler(promise, statusCode, response, next);
+    });
   app.delete('/v1/DeleteTest',
     function(request: any, response: any, next: any) {
       const args = {
diff --git a/tests/fixtures/hapi/routes.ts b/tests/fixtures/hapi/routes.ts
index 629182cb..40597208 100644
--- a/tests/fixtures/hapi/routes.ts
+++ b/tests/fixtures/hapi/routes.ts
@@ -65,9 +65,25 @@ const models: any = {
     'optionalPublicConstructorVar': { typeName: 'string', required: false },
     'id': { typeName: 'number', required: true },
   },
+  'GenericRequestTestModel': {
+    'name': { typeName: 'string', required: true },
+    'value': { typeName: 'TestModel', required: true },
+  },
   'Result': {
     'value': { typeName: 'object', required: true },
   },
+  'GenericModelTestModel': {
+    'result': { typeName: 'TestModel', required: true },
+  },
+  'GenericModelTestModel[]': {
+    'result': { typeName: 'array', required: true, arrayType: 'TestModel' },
+  },
+  'GenericModelstring': {
+    'result': { typeName: 'string', required: true },
+  },
+  'GenericModelstring[]': {
+    'result': { typeName: 'array', required: true, arrayType: 'string' },
+  },
   'ErrorResponseModel': {
     'status': { typeName: 'number', required: true },
     'message': { typeName: 'string', required: true },
@@ -384,6 +400,33 @@ export function RegisterRoutes(server: hapi.Server) {
       }
     }
   });
+  server.route({
+    method: 'post',
+    path: '/v1/PostTest/GenericBody',
+    config: {
+      handler: (request: any, reply) => {
+        const args = {
+          'genericReq': { name: 'genericReq', typeName: 'GenericRequestTestModel', required: true, in: 'body', },
+        };
+
+        let validatedArgs: any[] = [];
+        try {
+          validatedArgs = getValidatedArgs(args, request);
+        } catch (err) {
+          return reply(err).code(err.status || 500);
+        }
+
+        const controller = new PostTestController();
+
+        const promise = controller.getGenericRequest.apply(controller, validatedArgs);
+        let statusCode = undefined;
+        if (controller instanceof Controller) {
+          statusCode = (controller as Controller).getStatus();
+        }
+        return promiseHandler(promise, statusCode, request, reply);
+      }
+    }
+  });
   server.route({
     method: 'patch',
     path: '/v1/PatchTest',
@@ -812,6 +855,110 @@ export function RegisterRoutes(server: hapi.Server) {
       }
     }
   });
+  server.route({
+    method: 'get',
+    path: '/v1/GetTest/GenericModel',
+    config: {
+      handler: (request: any, reply) => {
+        const args = {
+        };
+
+        let validatedArgs: any[] = [];
+        try {
+          validatedArgs = getValidatedArgs(args, request);
+        } catch (err) {
+          return reply(err).code(err.status || 500);
+        }
+
+        const controller = new GetTestController();
+
+        const promise = controller.getGenericModel.apply(controller, validatedArgs);
+        let statusCode = undefined;
+        if (controller instanceof Controller) {
+          statusCode = (controller as Controller).getStatus();
+        }
+        return promiseHandler(promise, statusCode, request, reply);
+      }
+    }
+  });
+  server.route({
+    method: 'get',
+    path: '/v1/GetTest/GenericModelArray',
+    config: {
+      handler: (request: any, reply) => {
+        const args = {
+        };
+
+        let validatedArgs: any[] = [];
+        try {
+          validatedArgs = getValidatedArgs(args, request);
+        } catch (err) {
+          return reply(err).code(err.status || 500);
+        }
+
+        const controller = new GetTestController();
+
+        const promise = controller.getGenericModelArray.apply(controller, validatedArgs);
+        let statusCode = undefined;
+        if (controller instanceof Controller) {
+          statusCode = (controller as Controller).getStatus();
+        }
+        return promiseHandler(promise, statusCode, request, reply);
+      }
+    }
+  });
+  server.route({
+    method: 'get',
+    path: '/v1/GetTest/GenericPrimitive',
+    config: {
+      handler: (request: any, reply) => {
+        const args = {
+        };
+
+        let validatedArgs: any[] = [];
+        try {
+          validatedArgs = getValidatedArgs(args, request);
+        } catch (err) {
+          return reply(err).code(err.status || 500);
+        }
+
+        const controller = new GetTestController();
+
+        const promise = controller.getGenericPrimitive.apply(controller, validatedArgs);
+        let statusCode = undefined;
+        if (controller instanceof Controller) {
+          statusCode = (controller as Controller).getStatus();
+        }
+        return promiseHandler(promise, statusCode, request, reply);
+      }
+    }
+  });
+  server.route({
+    method: 'get',
+    path: '/v1/GetTest/GenericPrimitiveArray',
+    config: {
+      handler: (request: any, reply) => {
+        const args = {
+        };
+
+        let validatedArgs: any[] = [];
+        try {
+          validatedArgs = getValidatedArgs(args, request);
+        } catch (err) {
+          return reply(err).code(err.status || 500);
+        }
+
+        const controller = new GetTestController();
+
+        const promise = controller.getGenericPrimitiveArray.apply(controller, validatedArgs);
+        let statusCode = undefined;
+        if (controller instanceof Controller) {
+          statusCode = (controller as Controller).getStatus();
+        }
+        return promiseHandler(promise, statusCode, request, reply);
+      }
+    }
+  });
   server.route({
     method: 'delete',
     path: '/v1/DeleteTest',
@@ -1438,8 +1585,8 @@ export function RegisterRoutes(server: hapi.Server) {
       pre: [
         {
           method: authenticateMiddleware('api_key'
-          )
-        }
+          )        
+}
       ],
       handler: (request: any, reply) => {
         const args = {
@@ -1475,8 +1622,8 @@ export function RegisterRoutes(server: hapi.Server) {
               'write:pets',
               'read:pets'
             ]
-          )
-        }
+          )        
+}
       ],
       handler: (request: any, reply) => {
         const args = {
diff --git a/tests/fixtures/koa/routes.ts b/tests/fixtures/koa/routes.ts
index 9d522f3e..c2d48e8d 100644
--- a/tests/fixtures/koa/routes.ts
+++ b/tests/fixtures/koa/routes.ts
@@ -65,9 +65,25 @@ const models: any = {
     'optionalPublicConstructorVar': { typeName: 'string', required: false },
     'id': { typeName: 'number', required: true },
   },
+  'GenericRequestTestModel': {
+    'name': { typeName: 'string', required: true },
+    'value': { typeName: 'TestModel', required: true },
+  },
   'Result': {
     'value': { typeName: 'object', required: true },
   },
+  'GenericModelTestModel': {
+    'result': { typeName: 'TestModel', required: true },
+  },
+  'GenericModelTestModel[]': {
+    'result': { typeName: 'array', required: true, arrayType: 'TestModel' },
+  },
+  'GenericModelstring': {
+    'result': { typeName: 'string', required: true },
+  },
+  'GenericModelstring[]': {
+    'result': { typeName: 'array', required: true, arrayType: 'string' },
+  },
   'ErrorResponseModel': {
     'status': { typeName: 'number', required: true },
     'message': { typeName: 'string', required: true },
@@ -360,6 +376,31 @@ export function RegisterRoutes(router: KoaRouter) {
         statusCode = (controller as Controller).getStatus();
       }
 
+      return promiseHandler(promise, statusCode, context, next);
+    });
+  router.post('/v1/PostTest/GenericBody',
+    async (context, next) => {
+      const args = {
+        'genericReq': { name: 'genericReq', typeName: 'GenericRequestTestModel', required: true, in: 'body', },
+      };
+
+      let validatedArgs: any[] = [];
+      try {
+        validatedArgs = getValidatedArgs(args, context);
+      } catch (error) {
+        context.status = error.status || 500;
+        context.body = error;
+        return next();
+      }
+
+      const controller = new PostTestController();
+
+      const promise = controller.getGenericRequest.apply(controller, validatedArgs);
+      let statusCode = undefined;
+      if (controller instanceof Controller) {
+        statusCode = (controller as Controller).getStatus();
+      }
+
       return promiseHandler(promise, statusCode, context, next);
     });
   router.patch('/v1/PatchTest',
@@ -756,6 +797,102 @@ export function RegisterRoutes(router: KoaRouter) {
         statusCode = (controller as Controller).getStatus();
       }
 
+      return promiseHandler(promise, statusCode, context, next);
+    });
+  router.get('/v1/GetTest/GenericModel',
+    async (context, next) => {
+      const args = {
+      };
+
+      let validatedArgs: any[] = [];
+      try {
+        validatedArgs = getValidatedArgs(args, context);
+      } catch (error) {
+        context.status = error.status || 500;
+        context.body = error;
+        return next();
+      }
+
+      const controller = new GetTestController();
+
+      const promise = controller.getGenericModel.apply(controller, validatedArgs);
+      let statusCode = undefined;
+      if (controller instanceof Controller) {
+        statusCode = (controller as Controller).getStatus();
+      }
+
+      return promiseHandler(promise, statusCode, context, next);
+    });
+  router.get('/v1/GetTest/GenericModelArray',
+    async (context, next) => {
+      const args = {
+      };
+
+      let validatedArgs: any[] = [];
+      try {
+        validatedArgs = getValidatedArgs(args, context);
+      } catch (error) {
+        context.status = error.status || 500;
+        context.body = error;
+        return next();
+      }
+
+      const controller = new GetTestController();
+
+      const promise = controller.getGenericModelArray.apply(controller, validatedArgs);
+      let statusCode = undefined;
+      if (controller instanceof Controller) {
+        statusCode = (controller as Controller).getStatus();
+      }
+
+      return promiseHandler(promise, statusCode, context, next);
+    });
+  router.get('/v1/GetTest/GenericPrimitive',
+    async (context, next) => {
+      const args = {
+      };
+
+      let validatedArgs: any[] = [];
+      try {
+        validatedArgs = getValidatedArgs(args, context);
+      } catch (error) {
+        context.status = error.status || 500;
+        context.body = error;
+        return next();
+      }
+
+      const controller = new GetTestController();
+
+      const promise = controller.getGenericPrimitive.apply(controller, validatedArgs);
+      let statusCode = undefined;
+      if (controller instanceof Controller) {
+        statusCode = (controller as Controller).getStatus();
+      }
+
+      return promiseHandler(promise, statusCode, context, next);
+    });
+  router.get('/v1/GetTest/GenericPrimitiveArray',
+    async (context, next) => {
+      const args = {
+      };
+
+      let validatedArgs: any[] = [];
+      try {
+        validatedArgs = getValidatedArgs(args, context);
+      } catch (error) {
+        context.status = error.status || 500;
+        context.body = error;
+        return next();
+      }
+
+      const controller = new GetTestController();
+
+      const promise = controller.getGenericPrimitiveArray.apply(controller, validatedArgs);
+      let statusCode = undefined;
+      if (controller instanceof Controller) {
+        statusCode = (controller as Controller).getStatus();
+      }
+
       return promiseHandler(promise, statusCode, context, next);
     });
   router.delete('/v1/DeleteTest',
diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts
index d480fe26..c54942c4 100644
--- a/tests/fixtures/testModel.ts
+++ b/tests/fixtures/testModel.ts
@@ -114,3 +114,12 @@ export class TestClassModel extends TestClassBaseModel {
     super();
   }
 }
+
+export interface GenericModel<T> {
+  result: T;
+}
+
+export interface GenericRequest<T> {
+  name: string;
+  value: T;
+}
diff --git a/tests/integration/express-server.spec.ts b/tests/integration/express-server.spec.ts
index fc8b0d69..e9237eaf 100644
--- a/tests/integration/express-server.spec.ts
+++ b/tests/integration/express-server.spec.ts
@@ -1,6 +1,6 @@
 import 'mocha';
 import { app } from '../fixtures/express/server';
-import { TestModel, TestClassModel, UserResponseModel, ParameterTestModel } from '../fixtures/testModel';
+import { GenericModel, GenericRequest, TestModel, TestClassModel, UserResponseModel, ParameterTestModel } from '../fixtures/testModel';
 import * as chai from 'chai';
 import * as request from 'supertest';
 import { base64image } from '../fixtures/base64image';
@@ -265,6 +265,46 @@ describe('Express Server', () => {
         expect(model.human).to.equal(true);
       });
     });
+
+    it('can get request with generic type', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericModel', (err, res) => {
+        const model = res.body as GenericModel<TestModel>;
+        expect(model.result.id).to.equal(1);
+      });
+    });
+
+    it('can get request with generic array', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericModelArray', (err, res) => {
+        const model = res.body as GenericModel<TestModel[]>;
+        expect(model.result[0].id).to.equal(1);
+      });
+    });
+
+    it('can get request with generic primative type', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericPrimitive', (err, res) => {
+        const model = res.body as GenericModel<string>;
+        expect(model.result).to.equal('a string');
+      });
+    });
+
+    it('can get request with generic primative array', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericPrimitiveArray', (err, res) => {
+        const model = res.body as GenericModel<string[]>;
+        expect(model.result[0]).to.equal('string one');
+      });
+    });
+
+    it('can post request with a generic body', () => {
+
+      const data: GenericRequest<TestModel> = {
+        name: 'something',
+        value: getFakeModel()
+      };
+      return verifyPostRequest(basePath + '/PostTest/GenericBody', data, (err, res) => {
+        const model = res.body as TestModel;
+        expect(model.id).to.equal(1);
+      });
+    });
   });
 
   function verifyGetRequest(path: string, verifyResponse: (err: any, res: request.Response) => any, expectedStatus?: number) {
diff --git a/tests/integration/hapi-server.spec.ts b/tests/integration/hapi-server.spec.ts
index ba2b7e4b..1cb332f9 100644
--- a/tests/integration/hapi-server.spec.ts
+++ b/tests/integration/hapi-server.spec.ts
@@ -1,6 +1,6 @@
 import 'mocha';
 import { server } from '../fixtures/hapi/server';
-import { TestModel, TestClassModel, Model, ParameterTestModel } from '../fixtures/testModel';
+import { GenericModel, GenericRequest, TestModel, TestClassModel, Model, ParameterTestModel } from '../fixtures/testModel';
 import * as chai from 'chai';
 import * as request from 'supertest';
 
@@ -252,6 +252,46 @@ describe('Hapi Server', () => {
         expect(model.human).to.equal(true);
       });
     });
+
+    it('can get request with generic type', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericModel', (err, res) => {
+        const model = res.body as GenericModel<TestModel>;
+        expect(model.result.id).to.equal(1);
+      });
+    });
+
+    it('can get request with generic array', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericModelArray', (err, res) => {
+        const model = res.body as GenericModel<TestModel[]>;
+        expect(model.result[0].id).to.equal(1);
+      });
+    });
+
+    it('can get request with generic primative type', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericPrimitive', (err, res) => {
+        const model = res.body as GenericModel<string>;
+        expect(model.result).to.equal('a string');
+      });
+    });
+
+    it('can get request with generic primative array', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericPrimitiveArray', (err, res) => {
+        const model = res.body as GenericModel<string[]>;
+        expect(model.result[0]).to.equal('string one');
+      });
+    });
+
+    it('can post request with a generic body', () => {
+
+      const data: GenericRequest<TestModel> = {
+        name: 'something',
+        value: getFakeModel()
+      };
+      return verifyPostRequest(basePath + '/PostTest/GenericBody', data, (err, res) => {
+        const model = res.body as TestModel;
+        expect(model.id).to.equal(1);
+      });
+    });
   });
 
   function verifyGetRequest(path: string, verifyResponse: (err: any, res: request.Response) => any, expectedStatus?: number) {
diff --git a/tests/integration/koa-server.spec.ts b/tests/integration/koa-server.spec.ts
index dafc12ce..abb25e41 100644
--- a/tests/integration/koa-server.spec.ts
+++ b/tests/integration/koa-server.spec.ts
@@ -1,6 +1,6 @@
 import 'mocha';
 import { server } from '../fixtures/koa/server';
-import { TestModel, TestClassModel, Model, ParameterTestModel } from '../fixtures/testModel';
+import { GenericModel, GenericRequest, TestModel, TestClassModel, Model, ParameterTestModel } from '../fixtures/testModel';
 import * as chai from 'chai';
 import * as request from 'supertest';
 
@@ -230,6 +230,46 @@ describe('Koa Server', () => {
         expect(model.human).to.equal(true);
       });
     });
+
+    it('can get request with generic type', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericModel', (err, res) => {
+        const model = res.body as GenericModel<TestModel>;
+        expect(model.result.id).to.equal(1);
+      });
+    });
+
+    it('can get request with generic array', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericModelArray', (err, res) => {
+        const model = res.body as GenericModel<TestModel[]>;
+        expect(model.result[0].id).to.equal(1);
+      });
+    });
+
+    it('can get request with generic primative type', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericPrimitive', (err, res) => {
+        const model = res.body as GenericModel<string>;
+        expect(model.result).to.equal('a string');
+      });
+    });
+
+    it('can get request with generic primative array', () => {
+      return verifyGetRequest(basePath + '/GetTest/GenericPrimitiveArray', (err, res) => {
+        const model = res.body as GenericModel<string[]>;
+        expect(model.result[0]).to.equal('string one');
+      });
+    });
+
+    it('can post request with a generic body', () => {
+
+      const data: GenericRequest<TestModel> = {
+        name: 'something',
+        value: getFakeModel()
+      };
+      return verifyPostRequest(basePath + '/PostTest/GenericBody', data, (err, res) => {
+        const model = res.body as TestModel;
+        expect(model.id).to.equal(1);
+      });
+    });
   });
 
 
diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
index 0347bd29..566c53ea 100644
--- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
+++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
@@ -1,5 +1,6 @@
 import 'mocha';
 import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator';
+import {Swagger} from '../../../../src/swagger/swagger';
 import { SpecGenerator } from '../../../../src/swagger/specGenerator';
 import { getDefaultOptions } from '../../../fixtures/defaultOptions';
 import * as chai from 'chai';
@@ -167,4 +168,53 @@ describe('Definition generation', () => {
       expect(property.description).to.equal('This is a description for publicConstructorVar');
     });
   });
+
+  describe('Generic-based generation', () => {
+    it('should generate different definitions for a generic model', () => {
+      const definition = getValidatedDefinition('GenericModelTestModel').properties;
+
+      if (!definition) { throw new Error(`There were no properties on model.`); }
+
+      const property = definition['result'];
+
+      expect(property).to.exist;
+      expect(property['$ref']).to.equal('#/definitions/TestModel');
+    });
+    it('should generate different definitions for a generic model array', () => {
+      const definition = getValidatedDefinition('GenericModelTestModel[]').properties;
+
+      if (!definition) { throw new Error(`There were no properties on model.`); }
+
+      const property = definition['result'];
+
+      expect(property).to.exist;
+      expect(property.type).to.equal('array');
+
+      if (!property.items) { throw new Error(`There were no items on the property model.`); }
+      expect((property.items as Swagger.Schema)['$ref']).to.equal('#/definitions/TestModel');
+    });
+    it('should generate different definitions for a generic primitive', () => {
+      const definition = getValidatedDefinition('GenericModelstring').properties;
+
+      if (!definition) { throw new Error(`There were no properties on model.`); }
+
+      const property = definition['result'];
+
+      expect(property).to.exist;
+      expect(property.type).to.equal('string');
+    });
+    it('should generate different definitions for a generic primitive array', () => {
+      const definition = getValidatedDefinition('GenericModelstring[]').properties;
+
+      if (!definition) { throw new Error(`There were no properties on model.`); }
+
+      const property = definition['result'];
+
+      expect(property).to.exist;
+      expect(property.type).to.equal('array');
+
+      if (!property.items) { throw new Error(`There were no items on the property model.`); }
+      expect((property.items as Swagger.Schema)['type']).to.equal('string');
+    });
+  });
 });