Repository: lukeautry/tsoa
Pull request #69 (2017-03-03 21:10:55)

Support inject value from body properties
Example

`body message`
```json
{
firstname: 'Tony',
lastname: 'Stark'
age: 45
}
```

`Code`
```typescript
@Post('/helo')
public async hello(
    @BodyProp('firstname'): firstName,
    @BodyProp() lastname: string,
    @BodyProp() age: number): Promies<Model> {
    ....
}
```






Patch:
diff --git a/src/decorators/parameter.ts b/src/decorators/parameter.ts
index 22d8357b..ea7aa3f3 100644
--- a/src/decorators/parameter.ts
+++ b/src/decorators/parameter.ts
@@ -1,23 +1,50 @@
 /**
+ * Inject http Body
+ *  @param {string} [name] properties name in body object
+ */
+export function Body(): any {
+  return () => { return; };
+};
+
+/**
+ * Inject value from body
  *
- * @param {string} [propertiesName] - properties name in body object
+ * @param {string} [name] The name of the body parameter
  */
-export function Body(propertiesName?: string): any {
+export function BodyProp(name?: string): any {
   return () => { return; };
 };
 
+/**
+ * Inject http request
+ */
 export function Request(): any {
   return () => { return; };
 };
 
+/**
+ * Inject value from Path
+ *
+ * @param {string} [name] The name of the path parameter
+ */
 export function Path(name?: string): any {
   return () => { return; };
 };
 
+/**
+ * Inject value from query string
+ *
+ * @param {string} [name] The name of the query parameter
+ */
 export function Query(name?: string): any {
   return () => { return; };
 };
 
+/**
+ * Inject value from Http header
+ *
+ * @param {string} [name] The name of the header parameter
+ */
 export function Header(name?: string): any {
   return () => { return; };
 };
diff --git a/src/decorators/response.ts b/src/decorators/response.ts
index 337680a8..4d9204b4 100644
--- a/src/decorators/response.ts
+++ b/src/decorators/response.ts
@@ -1,9 +1,9 @@
 
 
-export function SuccessResponse(name: string, description?: string): any {
+export function SuccessResponse(name: string | number, description?: string): any {
   return () => { return; };
 };
 
-export function Response<T>(name: string, description?: string, example?: T): any {
+export function Response<T>(name: string | number, description?: string, example?: T): any {
   return () => { return; };
 };
diff --git a/src/index.ts b/src/index.ts
index 69af834c..1bad8688 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,5 @@
 import { Example } from './decorators/example';
-import { Request, Query, Path, Body, Header } from './decorators/parameter';
+import { Request, Query, Path, Body, BodyProp, Header } from './decorators/parameter';
 import { Post, Get, Patch, Delete, Put } from './decorators/methods';
 import { Tags } from './decorators/tags';
 import { Route } from './decorators/route';
@@ -19,6 +19,7 @@ export {
   Query,
   Path,
   Body,
+  BodyProp,
   Header,
   Response,
   SuccessResponse,
diff --git a/src/metadataGeneration/controllerGenerator.ts b/src/metadataGeneration/controllerGenerator.ts
index 4f00da6e..090d3342 100644
--- a/src/metadataGeneration/controllerGenerator.ts
+++ b/src/metadataGeneration/controllerGenerator.ts
@@ -51,7 +51,7 @@ export class ControllerGenerator {
 
     if (!matchedAttributes.length) { return undefined; }
     if (matchedAttributes.length > 1) {
-      throw new Error('A controller can only have a single "`decoratorName`" decorator.');
+      throw new Error(`A controller can only have a single 'decoratorName' decorator in \`${(this.node.name as any).text}\` class.`);
     }
 
     const value = matchedAttributes[0].arguments[0] as ts.StringLiteral;
diff --git a/src/metadataGeneration/methodGenerator.ts b/src/metadataGeneration/methodGenerator.ts
index b8440e87..5c3455b3 100644
--- a/src/metadataGeneration/methodGenerator.ts
+++ b/src/metadataGeneration/methodGenerator.ts
@@ -39,15 +39,33 @@ export class MethodGenerator {
   }
 
   private getParameters() {
-    return this.node.parameters.map(p => {
+    const parameters = this.node.parameters.map(p => {
       try {
         return new ParameterGenerator(p, this.method, this.path).Generate();
       } catch (e) {
         const methodId = this.node.name as ts.Identifier;
+        const controllerId = (this.node.parent as ts.ClassDeclaration).name as ts.Identifier;
         const parameterId = p.name as ts.Identifier;
-        throw new Error(`Error generate parameter method: ${methodId.text} argument: ${parameterId.text} ${e}`);
+        throw new Error(`Error generate parameter method: '${controllerId.text}.${methodId.text}' argument: ${parameterId.text} ${e}`);
       }
     });
+
+    const bodyParameters = parameters.filter(p => p.in === 'body');
+    const bodyProps = parameters.filter(p => p.in === 'body-prop');
+
+    if (bodyParameters.length > 1) {
+      throw new Error(`Only one body parameter allowed in '${this.getCurrentLocation()}' method.`);
+    }
+    if (bodyParameters.length > 0 && bodyProps.length > 0) {
+      throw new Error(`Choose either during @Body or @BodyProp in '${this.getCurrentLocation()}' method.`);
+    }
+    return parameters;
+  }
+
+  private getCurrentLocation() {
+    const methodId = this.node.name as ts.Identifier;
+    const controllerId = (this.node.parent as ts.ClassDeclaration).name as ts.Identifier;
+    return `${controllerId.text}.${methodId.text}`;
   }
 
   private processMethodDecorators() {
@@ -57,7 +75,7 @@ export class MethodGenerator {
 
     if (!pathDecorators || !pathDecorators.length) { return; }
     if (pathDecorators.length > 1) {
-      throw new Error(`Only one path decorator allowed per method. Found: ${pathDecorators.map(d => d.text).join(', ')}`);
+      throw new Error(`Only one path decorator in '${this.getCurrentLocation}' method, Found: ${pathDecorators.map(d => d.text).join(', ')}`);
     }
 
     const decorator = pathDecorators[0];
@@ -106,7 +124,9 @@ export class MethodGenerator {
         description: description,
         examples: examples,
         name: name,
-        schema: (expression.typeArguments && expression.typeArguments.length > 0) ? ResolveType(expression.typeArguments[0]) : undefined
+        schema: (expression.typeArguments && expression.typeArguments.length > 0)
+                ? ResolveType(expression.typeArguments[0])
+                : undefined
       };
     });
   }
@@ -121,7 +141,9 @@ export class MethodGenerator {
         schema: type
       };
     }
-    if (decorators.length > 1) { throw new Error('Only one SuccessResponse decorator allowed per controller method.'); }
+    if (decorators.length > 1) {
+      throw new Error(`Only one SuccessResponse decorator allowed in '${this.getCurrentLocation}' method.`);
+    }
 
     const decorator = decorators[0];
     const expression = decorator.parent as ts.CallExpression;
@@ -149,7 +171,7 @@ export class MethodGenerator {
     const exampleDecorators = this.getDecorators(identifier => identifier.text === 'Example');
     if (!exampleDecorators || !exampleDecorators.length) { return undefined; }
     if (exampleDecorators.length > 1) {
-      throw new Error('Only one Example decorator allowed per controller method.');
+      throw new Error(`Only one Example decorator allowed in '${this.getCurrentLocation}' method.`);
     }
 
     const decorator = exampleDecorators[0];
@@ -183,7 +205,9 @@ export class MethodGenerator {
   private getMethodTags() {
     const tagsDecorators = this.getDecorators(identifier => identifier.text === 'Tags');
     if (!tagsDecorators || !tagsDecorators.length) { return []; }
-    if (tagsDecorators.length > 1) { throw new Error('Only one Tags decorator allowed per controller method.'); }
+    if (tagsDecorators.length > 1) {
+      throw new Error(`Only one Tags decorator allowed in '${this.getCurrentLocation}' method.`);
+    }
 
     const decorator = tagsDecorators[0];
     const expression = decorator.parent as ts.CallExpression;
@@ -194,7 +218,9 @@ export class MethodGenerator {
   private getMethodSecurity() {
     const securityDecorators = this.getDecorators(identifier => identifier.text === 'Security');
     if (!securityDecorators || !securityDecorators.length) { return undefined; }
-    if (securityDecorators.length > 1) { throw new Error('Only one Security decorator allowed per controller method.'); }
+    if (securityDecorators.length > 1) {
+      throw new Error(`Only one Security decorator allowed in '${this.getCurrentLocation}' method.`);
+    }
 
     const decorator = securityDecorators[0];
     const expression = decorator.parent as ts.CallExpression;
diff --git a/src/metadataGeneration/parameterGenerator.ts b/src/metadataGeneration/parameterGenerator.ts
index 35b30e77..fd3fe422 100644
--- a/src/metadataGeneration/parameterGenerator.ts
+++ b/src/metadataGeneration/parameterGenerator.ts
@@ -17,6 +17,8 @@ export class ParameterGenerator {
         return this.getRequestParameter(this.parameter);
       case 'Body':
         return this.getBodyParameter(this.parameter);
+      case 'BodyProp':
+        return this.getBodyPropParameter(this.parameter);
       case 'Header':
         return this.getHeaderParameter(this.parameter);
       case 'Query':
@@ -28,6 +30,12 @@ export class ParameterGenerator {
     }
   }
 
+  private getCurrentLocation() {
+    const methodId = (this.parameter.parent as ts.MethodDeclaration).name as ts.Identifier;
+    const controllerId = ((this.parameter.parent as ts.MethodDeclaration).parent as ts.ClassDeclaration).name as ts.Identifier;
+    return `${controllerId.text}.${methodId.text}`;
+  }
+
   private getRequestParameter(parameter: ts.ParameterDeclaration): Parameter {
     const identifier = parameter.name as ts.Identifier;
 
@@ -41,6 +49,24 @@ export class ParameterGenerator {
     };
   }
 
+  private getBodyPropParameter(parameter: ts.ParameterDeclaration): Parameter {
+    const type = this.getValidatedType(parameter);
+    const identifier = parameter.name as ts.Identifier;
+
+    if (!this.supportsBodyParameters(this.method)) {
+      throw new Error(`Body can't support '${this.getCurrentLocation()}' method.`);
+    }
+
+    return {
+      argumentName: identifier.text,
+      description: this.getParameterDescription(parameter),
+      in: 'body-prop',
+      name: this.getDecoratorValue(parameter, 'BodyProp') || identifier.text,
+      required: !parameter.questionToken,
+      type: type
+    };
+  }
+
   private getBodyParameter(parameter: ts.ParameterDeclaration): Parameter {
     const type = this.getValidatedType(parameter);
     const identifier = parameter.name as ts.Identifier;
@@ -52,8 +78,8 @@ export class ParameterGenerator {
     return {
       argumentName: identifier.text,
       description: this.getParameterDescription(parameter),
-      in: this.getDecoratorValue(parameter, 'Body') ? 'body-props' : 'body',
-      name: this.getDecoratorValue(parameter, 'Body') || identifier.text,
+      in: 'body',
+      name: identifier.text,
       required: !parameter.questionToken,
       type: type
     };
@@ -64,7 +90,7 @@ export class ParameterGenerator {
     const identifier = parameter.name as ts.Identifier;
 
     if (!this.isPathableType(type)) {
-      throw new InvalidParameterException(`Parameter '${identifier.text}' can't be passed as a header parameter.`);
+      throw new InvalidParameterException(`Parameter '${identifier.text}' can't be passed as a header parameter in '${this.getCurrentLocation()}'.`);
     }
 
     return {
@@ -82,7 +108,7 @@ export class ParameterGenerator {
     const identifier = parameter.name as ts.Identifier;
 
     if (!this.isPathableType(type)) {
-      throw new InvalidParameterException(`Parameter '${identifier.text}' can't be passed as a query parameter.`);
+      throw new InvalidParameterException(`Parameter '${identifier.text}' can't be passed as a query parameter in '${this.getCurrentLocation()}'.`);
     }
 
     return {
@@ -100,7 +126,7 @@ export class ParameterGenerator {
     const identifier = parameter.name as ts.Identifier;
 
     if (!this.isPathableType(type)) {
-      throw new InvalidParameterException(`Parameter '${identifier.text}' can't be passed as a path parameter.`);
+      throw new InvalidParameterException(`Parameter '${identifier.text}' can't be passed as a path parameter in '${this.getCurrentLocation()}'.`);
     }
 
     const name = this.getDecoratorValue(parameter, 'Path') || identifier.text;
@@ -134,7 +160,7 @@ export class ParameterGenerator {
   }
 
   private supportParameterDecorator() {
-    return ['Header', 'Query', 'Parem', 'Body', 'Request'];
+    return ['Header', 'Query', 'Parem', 'Body', 'BodyProp', 'Request'];
   }
 
   private isPathableType(parameterType: Type) {
@@ -147,7 +173,9 @@ export class ParameterGenerator {
   }
 
   private getValidatedType(parameter: ts.ParameterDeclaration) {
-    if (!parameter.type) { throw new Error(`Parameter ${parameter.name} doesn't have a valid type assigned.`); }
+    if (!parameter.type) {
+      throw new Error(`Parameter ${parameter.name} doesn't have a valid type assigned in '${this.getCurrentLocation()}'.`);
+    }
     return ResolveType(parameter.type);
   }
 
diff --git a/src/metadataGeneration/resolveType.ts b/src/metadataGeneration/resolveType.ts
index 591a4136..ec333414 100644
--- a/src/metadataGeneration/resolveType.ts
+++ b/src/metadataGeneration/resolveType.ts
@@ -40,10 +40,12 @@ export function ResolveType(typeNode: ts.TypeNode): Type {
     return ResolveType(typeReference);
   }
 
-  return generateReferenceType(typeReference.typeName.text);
+  const referenceType = generateReferenceType(typeReference.typeName.text);
+  MetadataGenerator.current.AddReferenceType(referenceType);
+  return referenceType;
 }
 
-function generateReferenceType(typeName: string, cacheReferenceType = true): ReferenceType {
+function generateReferenceType(typeName: string): ReferenceType {
   try {
     const existingType = localReferenceTypeCache[typeName];
     if (existingType) { return existingType; }
@@ -73,10 +75,6 @@ function generateReferenceType(typeName: string, cacheReferenceType = true): Ref
     const extendedProperties = getInheritedProperties(modelTypeDeclaration);
     referenceType.properties = referenceType.properties.concat(extendedProperties);
 
-    if (cacheReferenceType) {
-      MetadataGenerator.current.AddReferenceType(referenceType);
-    }
-
     localReferenceTypeCache[typeName] = referenceType;
     return referenceType;
   } catch (err) {
@@ -207,7 +205,7 @@ function getInheritedProperties(modelTypeDeclaration: UsableDeclaration): Proper
 
     clause.types.forEach(t => {
       const baseIdentifier = t.expression as ts.Identifier;
-      generateReferenceType(baseIdentifier.text, false).properties
+      generateReferenceType(baseIdentifier.text).properties
         .forEach(property => properties.push(property));
     });
   });
diff --git a/src/routeGeneration/templates/express.ts b/src/routeGeneration/templates/express.ts
index f36fe554..a417cacc 100644
--- a/src/routeGeneration/templates/express.ts
+++ b/src/routeGeneration/templates/express.ts
@@ -96,7 +96,7 @@ export function RegisterRoutes(app: any) {
                 return ValidateParam(args[key], request.header(name), models, name);
             case 'body':
                 return ValidateParam(args[key], request.body, models, name);
-            case 'body-props':
+            case 'body-prop':
                 return ValidateParam(args[key], request.body[name], models, name);
             }
         });
diff --git a/src/routeGeneration/templates/hapi.ts b/src/routeGeneration/templates/hapi.ts
index b4d0b0d3..e91dd828 100644
--- a/src/routeGeneration/templates/hapi.ts
+++ b/src/routeGeneration/templates/hapi.ts
@@ -100,7 +100,7 @@ export function RegisterRoutes(server: hapi.Server) {
                 return ValidateParam(args[key], request.headers[name], models, name);
             case 'body':
                 return ValidateParam(args[key], request.payload, models, name);
-             case 'body-props':
+             case 'body-prop':
                 return ValidateParam(args[key], request.payload[name], models, name);
             }
         });
diff --git a/src/routeGeneration/templates/koa.ts b/src/routeGeneration/templates/koa.ts
index 46f97475..8724b5fd 100644
--- a/src/routeGeneration/templates/koa.ts
+++ b/src/routeGeneration/templates/koa.ts
@@ -105,7 +105,7 @@ export function RegisterRoutes(router: KoaRouter) {
                 return ValidateParam(args[key], context.request.headers[name], models, name);
             case 'body':
                 return ValidateParam(args[key], context.request.body, models, name);
-            case 'body-props':
+            case 'body-prop':
                 return ValidateParam(args[key], context.request.body[name], models, name);
             }
         });
diff --git a/src/swagger/specGenerator.ts b/src/swagger/specGenerator.ts
index eceb3329..6feb20b4 100644
--- a/src/swagger/specGenerator.ts
+++ b/src/swagger/specGenerator.ts
@@ -102,45 +102,50 @@ export class SpecGenerator {
       pathMethod.security = [security];
     }
 
-    pathMethod.parameters = method.parameters.filter(
-      p => !(p.in === 'request' || p.in === 'body-props')
-    ).map(p => this.buildParameter(p));
-    const bodyPropParameter = this.buildBodyPropParamter(method);
+    pathMethod.parameters = method.parameters
+      .filter(p => {
+        return !(p.in === 'request' || p.in === 'body-prop');
+      })
+      .map(p => this.buildParameter(p));
+
+    const bodyPropParameter = this.buildBodyPropParameter(method);
     if (bodyPropParameter) {
-        pathMethod.parameters.push(bodyPropParameter);
-     }
+      pathMethod.parameters.push(bodyPropParameter);
+    }
 
     if (pathMethod.parameters.filter((p: Swagger.BaseParameter) => p.in === 'body').length > 1) {
       throw new Error('Only one body parameter allowed per controller method.');
     }
   }
 
-  private buildBodyPropParamter(method: Method) {
-    const bodyProperties: any = {};
-    const bodyRequired: string[] = [];
+  private buildBodyPropParameter(method: Method) {
+    const properties: any = {};
+    const required: string[] = [];
+
     method.parameters
-      .filter(p => p.in === 'body-props')
+      .filter(p => p.in === 'body-prop')
       .forEach(p => {
-        bodyProperties[p.name] = this.getSwaggerType(p.type);
-        bodyProperties[p.name].description = p.description;
+        properties[p.name] = this.getSwaggerType(p.type);
+        properties[p.name].description = p.description;
 
-        if (p.required) { bodyRequired.push(p.name); }
+        if (p.required) { required.push(p.name); }
       });
 
-    if (Object.keys(bodyProperties).length) {
-      return {
-        in: 'body',
-        name: 'body-props-object',
-        schema: {
-          properties: bodyProperties,
-          required: bodyRequired,
-          title: 'body-props-object',
-          type: 'object'
-        }
-      };
-    } else {
-      return;
+    if (!Object.keys(properties).length) { return; };
+
+    const parameter: any = {
+      in: 'body',
+      name: 'body-inline-paramater',
+      schema: {
+        properties: properties,
+        title: 'inline-schema',
+        type: 'object'
+      }
+    };
+    if (required.length) {
+      parameter.schema.required = required;
     }
+    return parameter;
   }
 
   private buildParameter(parameter: Parameter): Swagger.Parameter {


Test patch:
diff --git a/tests/fixtures/controllers/parameterController.ts b/tests/fixtures/controllers/parameterController.ts
index 9b67c3f6..278936ed 100644
--- a/tests/fixtures/controllers/parameterController.ts
+++ b/tests/fixtures/controllers/parameterController.ts
@@ -1,4 +1,4 @@
-import { Request, Query, Path, Header, Body } from '../../../src/decorators/parameter';
+import { Request, Query, Path, Header, Body, BodyProp } from '../../../src/decorators/parameter';
 import { Get, Post } from '../../../src/decorators/methods';
 import { Route } from '../../../src/decorators/route';
 import { ParameterTestModel } from '../testModel';
@@ -114,10 +114,10 @@ export class ParameterController {
      */
     @Post('BodyProps')
     public async getBodyProps(
-        @Body('firstname') firstname: string,
-        @Body('lastname') lastname: string,
-        @Body('human') human: boolean,
-        @Body('age') age: number): Promise<ParameterTestModel> {
+        @BodyProp('firstname') firstname: string,
+        @BodyProp('lastname') lastname: string,
+        @BodyProp('human') human: boolean,
+        @BodyProp('age') age: number): Promise<ParameterTestModel> {
         return Promise.resolve<ParameterTestModel>({
             age: age,
             firstname: firstname,
diff --git a/tests/fixtures/express/routes.ts b/tests/fixtures/express/routes.ts
index 6a883132..cc9f7e05 100644
--- a/tests/fixtures/express/routes.ts
+++ b/tests/fixtures/express/routes.ts
@@ -11,13 +11,6 @@ import { ParameterController } from './../controllers/parameterController';
 import { SecurityTestController } from './../controllers/securityController';
 
 const models: any = {
-  'TestSubModel': {
-    'email': { typeName: 'string', required: true },
-    'circular': { typeName: 'TestModel', required: false },
-    'id': { typeName: 'number', required: true },
-  },
-  'StrLiteral': {
-  },
   'TestModel': {
     'numberValue': { typeName: 'number', required: true },
     'numberArray': { typeName: 'array', required: true, arrayType: 'number' },
@@ -33,6 +26,13 @@ const models: any = {
     'optionalString': { typeName: 'string', required: false },
     'id': { typeName: 'number', required: true },
   },
+  'TestSubModel': {
+    'email': { typeName: 'string', required: true },
+    'circular': { typeName: 'TestModel', required: false },
+    'id': { typeName: 'number', required: true },
+  },
+  'StrLiteral': {
+  },
   'TestClassModel': {
     'publicStringProperty': { typeName: 'string', required: true },
     'optionalPublicStringProperty': { typeName: 'string', required: false },
@@ -1150,10 +1150,10 @@ export function RegisterRoutes(app: any) {
   app.post('/v1/ParameterTest/BodyProps',
     function(request: any, response: any, next: any) {
       const args = {
-        'firstname': { name: 'firstname', typeName: 'string', required: true, in: 'body-props', },
-        'lastname': { name: 'lastname', typeName: 'string', required: true, in: 'body-props', },
-        'human': { name: 'human', typeName: 'boolean', required: true, in: 'body-props', },
-        'age': { name: 'age', typeName: 'number', required: true, in: 'body-props', },
+        'firstname': { name: 'firstname', typeName: 'string', required: true, in: 'body-prop', },
+        'lastname': { name: 'lastname', typeName: 'string', required: true, in: 'body-prop', },
+        'human': { name: 'human', typeName: 'boolean', required: true, in: 'body-prop', },
+        'age': { name: 'age', typeName: 'number', required: true, in: 'body-prop', },
       };
 
       let validatedArgs: any[] = [];
@@ -1294,7 +1294,7 @@ export function RegisterRoutes(app: any) {
           return ValidateParam(args[key], request.header(name), models, name);
         case 'body':
           return ValidateParam(args[key], request.body, models, name);
-        case 'body-props':
+        case 'body-prop':
           return ValidateParam(args[key], request.body[name], models, name);
       }
     });
diff --git a/tests/fixtures/hapi/routes.ts b/tests/fixtures/hapi/routes.ts
index ae7b91a1..b0bb070a 100644
--- a/tests/fixtures/hapi/routes.ts
+++ b/tests/fixtures/hapi/routes.ts
@@ -11,13 +11,6 @@ import { ParameterController } from './../controllers/parameterController';
 import { SecurityTestController } from './../controllers/securityController';
 
 const models: any = {
-  'TestSubModel': {
-    'email': { typeName: 'string', required: true },
-    'circular': { typeName: 'TestModel', required: false },
-    'id': { typeName: 'number', required: true },
-  },
-  'StrLiteral': {
-  },
   'TestModel': {
     'numberValue': { typeName: 'number', required: true },
     'numberArray': { typeName: 'array', required: true, arrayType: 'number' },
@@ -33,6 +26,13 @@ const models: any = {
     'optionalString': { typeName: 'string', required: false },
     'id': { typeName: 'number', required: true },
   },
+  'TestSubModel': {
+    'email': { typeName: 'string', required: true },
+    'circular': { typeName: 'TestModel', required: false },
+    'id': { typeName: 'number', required: true },
+  },
+  'StrLiteral': {
+  },
   'TestClassModel': {
     'publicStringProperty': { typeName: 'string', required: true },
     'optionalPublicStringProperty': { typeName: 'string', required: false },
@@ -1350,10 +1350,10 @@ export function RegisterRoutes(server: hapi.Server) {
     config: {
       handler: (request: any, reply) => {
         const args = {
-          'firstname': { name: 'firstname', typeName: 'string', required: true, in: 'body-props', },
-          'lastname': { name: 'lastname', typeName: 'string', required: true, in: 'body-props', },
-          'human': { name: 'human', typeName: 'boolean', required: true, in: 'body-props', },
-          'age': { name: 'age', typeName: 'number', required: true, in: 'body-props', },
+          'firstname': { name: 'firstname', typeName: 'string', required: true, in: 'body-prop', },
+          'lastname': { name: 'lastname', typeName: 'string', required: true, in: 'body-prop', },
+          'human': { name: 'human', typeName: 'boolean', required: true, in: 'body-prop', },
+          'age': { name: 'age', typeName: 'number', required: true, in: 'body-prop', },
         };
 
         let validatedArgs: any[] = [];
@@ -1514,7 +1514,7 @@ export function RegisterRoutes(server: hapi.Server) {
           return ValidateParam(args[key], request.headers[name], models, name);
         case 'body':
           return ValidateParam(args[key], request.payload, models, name);
-        case 'body-props':
+        case 'body-prop':
           return ValidateParam(args[key], request.payload[name], models, name);
       }
     });
diff --git a/tests/fixtures/inversify/routes.ts b/tests/fixtures/inversify/routes.ts
index 6f84c87c..3849886d 100644
--- a/tests/fixtures/inversify/routes.ts
+++ b/tests/fixtures/inversify/routes.ts
@@ -5,13 +5,6 @@ import { iocContainer } from './ioc';
 import { ManagedController } from './managedController';
 
 const models: any = {
-  'TestSubModel': {
-    'email': { typeName: 'string', required: true },
-    'circular': { typeName: 'TestModel', required: false },
-    'id': { typeName: 'number', required: true },
-  },
-  'StrLiteral': {
-  },
   'TestModel': {
     'numberValue': { typeName: 'number', required: true },
     'numberArray': { typeName: 'array', required: true, arrayType: 'number' },
@@ -27,6 +20,13 @@ const models: any = {
     'optionalString': { typeName: 'string', required: false },
     'id': { typeName: 'number', required: true },
   },
+  'TestSubModel': {
+    'email': { typeName: 'string', required: true },
+    'circular': { typeName: 'TestModel', required: false },
+    'id': { typeName: 'number', required: true },
+  },
+  'StrLiteral': {
+  },
 };
 
 
@@ -84,7 +84,7 @@ export function RegisterRoutes(app: any) {
           return ValidateParam(args[key], request.header(name), models, name);
         case 'body':
           return ValidateParam(args[key], request.body, models, name);
-        case 'body-props':
+        case 'body-prop':
           return ValidateParam(args[key], request.body[name], models, name);
       }
     });
diff --git a/tests/fixtures/koa/routes.ts b/tests/fixtures/koa/routes.ts
index f4874ac0..ef10b35b 100644
--- a/tests/fixtures/koa/routes.ts
+++ b/tests/fixtures/koa/routes.ts
@@ -11,13 +11,6 @@ import { ParameterController } from './../controllers/parameterController';
 import { SecurityTestController } from './../controllers/securityController';
 
 const models: any = {
-  'TestSubModel': {
-    'email': { typeName: 'string', required: true },
-    'circular': { typeName: 'TestModel', required: false },
-    'id': { typeName: 'number', required: true },
-  },
-  'StrLiteral': {
-  },
   'TestModel': {
     'numberValue': { typeName: 'number', required: true },
     'numberArray': { typeName: 'array', required: true, arrayType: 'number' },
@@ -33,6 +26,13 @@ const models: any = {
     'optionalString': { typeName: 'string', required: false },
     'id': { typeName: 'number', required: true },
   },
+  'TestSubModel': {
+    'email': { typeName: 'string', required: true },
+    'circular': { typeName: 'TestModel', required: false },
+    'id': { typeName: 'number', required: true },
+  },
+  'StrLiteral': {
+  },
   'TestClassModel': {
     'publicStringProperty': { typeName: 'string', required: true },
     'optionalPublicStringProperty': { typeName: 'string', required: false },
@@ -1245,10 +1245,10 @@ export function RegisterRoutes(router: KoaRouter) {
   router.post('/v1/ParameterTest/BodyProps',
     async (context, next) => {
       const args = {
-        'firstname': { name: 'firstname', typeName: 'string', required: true, in: 'body-props', },
-        'lastname': { name: 'lastname', typeName: 'string', required: true, in: 'body-props', },
-        'human': { name: 'human', typeName: 'boolean', required: true, in: 'body-props', },
-        'age': { name: 'age', typeName: 'number', required: true, in: 'body-props', },
+        'firstname': { name: 'firstname', typeName: 'string', required: true, in: 'body-prop', },
+        'lastname': { name: 'lastname', typeName: 'string', required: true, in: 'body-prop', },
+        'human': { name: 'human', typeName: 'boolean', required: true, in: 'body-prop', },
+        'age': { name: 'age', typeName: 'number', required: true, in: 'body-prop', },
       };
 
       let validatedArgs: any[] = [];
@@ -1403,7 +1403,7 @@ export function RegisterRoutes(router: KoaRouter) {
           return ValidateParam(args[key], context.request.headers[name], models, name);
         case 'body':
           return ValidateParam(args[key], context.request.body, models, name);
-        case 'body-props':
+        case 'body-prop':
           return ValidateParam(args[key], context.request.body[name], models, name);
       }
     });
diff --git a/tests/unit/swagger/definitionsGeneration/metadata.spec.ts b/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
index c4f44850..1e177b5f 100644
--- a/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
+++ b/tests/unit/swagger/definitionsGeneration/metadata.spec.ts
@@ -359,7 +359,7 @@ describe('Metadata generation', () => {
 
       expect(method.parameters.length).to.equal(4);
       expect(parameter.description).to.equal('firstname description');
-      expect(parameter.in).to.equal('body-props');
+      expect(parameter.in).to.equal('body-prop');
       expect(parameter.name).to.equal('firstname');
       expect(parameter.argumentName).to.equal('firstname');
       expect(parameter.required).to.be.true;
diff --git a/tests/unit/swagger/pathGeneration/getRoutes.spec.ts b/tests/unit/swagger/pathGeneration/getRoutes.spec.ts
index fabc0cc9..6c1e587c 100644
--- a/tests/unit/swagger/pathGeneration/getRoutes.spec.ts
+++ b/tests/unit/swagger/pathGeneration/getRoutes.spec.ts
@@ -120,7 +120,7 @@ describe('GET route generation', () => {
     expect(() => {
       const invalidMetadata = new MetadataGenerator('./tests/fixtures/controllers/invalidGetController.ts').Generate();
       new SpecGenerator(invalidMetadata, getDefaultOptions()).GetSpec();
-    }).to.throw('Parameter \'myModel\' can\'t be passed as a query parameter.');
+    }).to.throw('Parameter \'myModel\' can\'t be passed as a query parameter in \'InvalidGetTestController.getModelWithComplex\'.');
   });
 
   it('should generate a path description from jsdoc comment', () => {
diff --git a/tests/unit/swagger/pathGeneration/postRoutes.spec.ts b/tests/unit/swagger/pathGeneration/postRoutes.spec.ts
index 945a2914..05fc8db7 100644
--- a/tests/unit/swagger/pathGeneration/postRoutes.spec.ts
+++ b/tests/unit/swagger/pathGeneration/postRoutes.spec.ts
@@ -48,7 +48,7 @@ describe('POST route generation', () => {
     chai.expect(() => {
       const invalidMetadata = new MetadataGenerator('./tests/fixtures/controllers/invalidPostController.ts').Generate();
       new SpecGenerator(invalidMetadata, getDefaultOptions()).GetSpec();
-    }).to.throw('Only one body parameter allowed per controller method.');
+    }).to.throw('Only one body parameter allowed in \'InvalidPostTestController.postWithMultipleBodyParams\' method.');
   });
 
   it('should be able to parse body and query parameters together', () => {