Repository: lukeautry/tsoa
Pull request #249 (2018-07-27 13:43:55)

Spec generator for OpenAPI 3.0.0
This is a first version of a spec generator for OpenAPI 3.0.0. Please check it out and let me know if this makes sense of if it should be done some other way. As you can see from the TODO at the top, not everything is done yet, but it should be able to produce most of the things that the current spec generator can and I guess users can always switch back if it doesn't yet support something they need. My thinking is that the sooner this gets merged the easier it'll be for other people to extend.

Pinging the other people that want to help maintain tsoa:
@AmazingTurtle @jamestharpe @0v3rst33r @abmohan

Here are examples of v2 vs. v3:
https://mermade.org.uk/examples/swagger.json
https://mermade.org.uk/examples/openapi.json

And here are some more useful links:
https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md
https://swagger.io/blog/news/whats-new-in-openapi-3-0/
https://blog.readme.io/an-example-filled-guide-to-swagger-3-2/

lukeautry: Been meaning to look at this - hopefully I can get to it soon. I like the approach of maintaining support for both, despite the added complexity.
lukeautry: @endor These are some kind of nasty merge conflicts - if you can figure out the merge I'd be happy to get this merged in. It would be great to say we support Swagger 3.0.
endor: Conflicts fixed. I'm not sure we can say we support OpenAPI 3.0.0 after this, but it's at least a big step closer. Then people can start implementing the missing features and I guess if more people start using it to generate 3.0.0 specs we'll also notice potential bugs (:
endor: Fixed again. @lukeautry should we merge this?
lukeautry: @endor Sorry for the delay on this - if you can fix the merge conflicts, please merge right away and I'll get it into a release. I think this will solve several issues at once, and even if it's not 100% supported, we have a good jumping off point.
leolorenzoluis: @endor Would you be able to merge the conflicts please? 💯 
dgreene1: @endor would you be willing to resolve the conflicts again?
endor: I will try to get to it on Monday or Tuesday.
endor: Since this has been going through 3 rebases, I would love for someone to check this before I or someone else merges it.
0v3rst33r: I would gladly have a look at it @endor (will probably only get to it tomorrow evening if that is ok).

I want to get into the development effort here, and this seems like as good an opportunity as any 😄 
lukeautry: Thanks @0v3rst33r. I don't want to merge anything else until this goes in since it has been open for so long, so if someone familiar with OpenAPI 3.0 can verify the functionality here, we can get it merged.
dgreene1: @lukeautry what if we just merged this as is?

I think this would be safe enough to do since the code defaults to version 2, so that would be non-breaking for current `tsoa` users. Additionally, those who are excited enough to try out `tsoa` with version 3, would be able to submit issues to us (which would get us the valuable feedback we want). Essentially we could merge this as beta functionality. It seems like the most Agile thing to do. And plus we would then have the PR merged and we could stop worrying about rebasing (I know that would make @endor happy, I bet 😃).
dgreene1: @endor, @lukeautry any thoughts on the “give it a shot”/ Agile approach I’m recommending above? https://github.com/lukeautry/tsoa/pull/249#issuecomment-496583438
endor: From me it's obviously a go ;)

Patch:
diff --git a/src/metadataGeneration/tsoa.ts b/src/metadataGeneration/tsoa.ts
index 8b19de91..729ca3dd 100644
--- a/src/metadataGeneration/tsoa.ts
+++ b/src/metadataGeneration/tsoa.ts
@@ -25,7 +25,6 @@ export namespace Tsoa {
     summary?: string;
     isHidden: boolean;
     operationId?: string;
-
   }
 
   export interface Parameter {
diff --git a/src/module/generate-swagger-spec.ts b/src/module/generate-swagger-spec.ts
index 5bdef2af..ea3fae74 100644
--- a/src/module/generate-swagger-spec.ts
+++ b/src/module/generate-swagger-spec.ts
@@ -3,7 +3,9 @@ import * as YAML from 'yamljs';
 import { SwaggerConfig } from '../config';
 import { MetadataGenerator } from '../metadataGeneration/metadataGenerator';
 import { Tsoa } from '../metadataGeneration/tsoa';
-import { SpecGenerator } from '../swagger/specGenerator';
+import { SpecGenerator2 } from '../swagger/specGenerator2';
+import { SpecGenerator3 } from '../swagger/specGenerator3';
+import { Swagger } from '../swagger/swagger';
 import { fsExists, fsMkDir, fsWriteFile } from '../utils/fs';
 
 export const generateSwaggerSpec = async (
@@ -22,7 +24,13 @@ export const generateSwaggerSpec = async (
       ignorePaths,
     ).Generate();
   }
-  const spec = new SpecGenerator(metadata, config).GetSpec();
+
+  let spec: Swagger.Spec;
+  if (config.version && config.version.split('.')[0] === '3') {
+    spec = new SpecGenerator3(metadata, config).GetSpec();
+  } else {
+    spec = new SpecGenerator2(metadata, config).GetSpec();
+  }
 
   const exists = await fsExists(config.outputDirectory);
   if (!exists) {
diff --git a/src/swagger/specGenerator.ts b/src/swagger/specGenerator.ts
index 5d88a6a1..0334b012 100644
--- a/src/swagger/specGenerator.ts
+++ b/src/swagger/specGenerator.ts
@@ -1,260 +1,15 @@
 import { Tsoa } from '../metadataGeneration/tsoa';
 import { SwaggerConfig } from './../config';
-import { normalisePath } from './../utils/pathUtils';
 import { Swagger } from './swagger';
 
 export class SpecGenerator {
-  constructor(private readonly metadata: Tsoa.Metadata, private readonly config: SwaggerConfig) { }
+  constructor(protected readonly metadata: Tsoa.Metadata, protected readonly config: SwaggerConfig) { }
 
-  public GetSpec() {
-    let spec: Swagger.Spec = {
-      basePath: normalisePath(this.config.basePath as string, '/', undefined, false),
-      consumes: ['application/json'],
-      definitions: this.buildDefinitions(),
-      info: {
-        title: '',
-      },
-      paths: this.buildPaths(),
-      produces: ['application/json'],
-      swagger: '2.0',
-    };
-
-    spec.securityDefinitions = this.config.securityDefinitions
-      ? this.config.securityDefinitions
-      : {};
-
-    if (this.config.name) { spec.info.title = this.config.name; }
-    if (this.config.version) { spec.info.version = this.config.version; }
-    if (this.config.host) { spec.host = this.config.host; }
-    if (this.config.description) { spec.info.description = this.config.description; }
-    if (this.config.license) { spec.info.license = { name: this.config.license }; }
-    if (this.config.tags) { spec.tags = this.config.tags; }
-    if (this.config.spec) {
-      this.config.specMerging = this.config.specMerging || 'immediate';
-      const mergeFuncs: { [key: string]: any } = {
-        immediate: Object.assign,
-        recursive: require('merge').recursive,
-      };
-
-      spec = mergeFuncs[this.config.specMerging](spec, this.config.spec);
-    }
-    if (this.config.schemes) { spec.schemes = this.config.schemes; }
-
-    return spec;
-  }
-
-  private buildDefinitions() {
-    const definitions: { [definitionsName: string]: Swagger.Schema } = {};
-    Object.keys(this.metadata.referenceTypeMap).map(typeName => {
-      const referenceType = this.metadata.referenceTypeMap[typeName];
-
-      // Object definition
-      if (referenceType.properties) {
-        const required = referenceType.properties.filter(p => p.required).map(p => p.name);
-        definitions[referenceType.refName] = {
-          description: referenceType.description,
-          properties: this.buildProperties(referenceType.properties),
-          required: required && required.length > 0 ? Array.from(new Set(required)) : undefined,
-          type: 'object',
-        };
-
-        if (referenceType.additionalProperties) {
-          definitions[referenceType.refName].additionalProperties = this.buildAdditionalProperties(referenceType.additionalProperties);
-        }
-
-        if (referenceType.example) {
-          definitions[referenceType.refName].example = referenceType.example;
-        }
-      }
-
-      // Enum definition
-      if (referenceType.enums) {
-        definitions[referenceType.refName] = {
-          description: referenceType.description,
-          enum: referenceType.enums,
-          type: 'string',
-        };
-      }
-    });
-
-    return definitions;
-  }
-
-  private buildPaths() {
-    const paths: { [pathName: string]: Swagger.Path } = {};
-
-    this.metadata.controllers.forEach(controller => {
-      const normalisedControllerPath = normalisePath(controller.path, '/');
-      // construct documentation using all methods except @Hidden
-      controller.methods.filter(method => !method.isHidden).forEach(method => {
-        const normalisedMethodPath = normalisePath(method.path, '/');
-        const path = normalisePath(`${normalisedControllerPath}${normalisedMethodPath}`, '/', '', false);
-        paths[path] = paths[path] || {};
-        this.buildMethod(controller.name, method, paths[path]);
-      });
-    });
-
-    return paths;
-  }
-
-  private buildMethod(controllerName: string, method: Tsoa.Method, pathObject: any) {
-    const pathMethod: Swagger.Operation = pathObject[method.method] = this.buildOperation(controllerName, method);
-    pathMethod.description = method.description;
-    pathMethod.summary = method.summary;
-    pathMethod.tags = method.tags;
-
-    // Use operationId tag otherwise fallback to generated. Warning: This doesn't check uniqueness.
-    pathMethod.operationId = method.operationId || pathMethod.operationId;
-
-    if (method.deprecated) {
-      pathMethod.deprecated = method.deprecated;
-    }
-
-    if (method.security) {
-      pathMethod.security = method.security as any[];
-    }
-
-    pathMethod.parameters = method.parameters
-      .filter(p => {
-        return !(p.in === 'request' || p.in === 'body-prop');
-      })
-      .map(p => this.buildParameter(p));
-
-    const bodyPropParameter = this.buildBodyPropParameter(controllerName, method);
-    if (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 buildBodyPropParameter(controllerName: string, method: Tsoa.Method) {
-    const properties = {} as { [name: string]: Swagger.Schema };
-    const required: string[] = [];
-
-    method.parameters
-      .filter(p => p.in === 'body-prop')
-      .forEach(p => {
-        properties[p.name] = this.getSwaggerType(p.type);
-        properties[p.name].default = p.default;
-        properties[p.name].description = p.description;
-
-        if (p.required) {
-          required.push(p.name);
-        }
-      });
-
-    if (!Object.keys(properties).length) { return; }
-
-    const parameter = {
-      in: 'body',
-      name: 'body',
-      schema: {
-        properties,
-        title: `${this.getOperationId(method.name)}Body`,
-        type: 'object',
-      },
-    } as Swagger.Parameter;
-    if (required.length) {
-      parameter.schema.required = required;
-    }
-    return parameter;
-  }
-
-  private buildParameter(source: Tsoa.Parameter): Swagger.Parameter {
-    let parameter = {
-      default: source.default,
-      description: source.description,
-      in: source.in,
-      name: source.name,
-      required: source.required,
-    } as Swagger.Parameter;
-
-    const parameterType = this.getSwaggerType(source.type);
-    parameter.format = parameterType.format || undefined;
-
-    if (parameter.in === 'query' && parameterType.type === 'array') {
-      (parameter as Swagger.QueryParameter).collectionFormat = 'multi';
-    }
-
-    if (parameterType.$ref) {
-      parameter.schema = parameterType as Swagger.Schema;
-      return parameter;
-    }
-
-    const validatorObjs = {};
-    Object.keys(source.validators)
-      .filter(key => {
-        return !key.startsWith('is') && key !== 'minDate' && key !== 'maxDate';
-      })
-      .forEach((key: string) => {
-        validatorObjs[key] = source.validators[key].value;
-      });
-
-    if (source.in === 'body' && source.type.dataType === 'array') {
-      parameter.schema = {
-        items: parameterType.items,
-        type: 'array',
-      };
-    } else {
-      if (source.type.dataType === 'any') {
-        if (source.in === 'body') {
-          parameter.schema = { type: 'object' };
-        } else {
-          parameter.type = 'string';
-        }
-      } else {
-        parameter.type = parameterType.type;
-        parameter.items = parameterType.items;
-        parameter.enum = parameterType.enum;
-      }
-    }
-
-    if (parameter.schema) {
-      parameter.schema = Object.assign({}, parameter.schema, validatorObjs);
-    } else {
-      parameter = Object.assign({}, parameter, validatorObjs);
-    }
-
-    return parameter;
-  }
-
-  private buildProperties(source: Tsoa.Property[]) {
-    const properties: { [propertyName: string]: Swagger.Schema } = {};
-
-    source.forEach(property => {
-      const swaggerType = this.getSwaggerType(property.type);
-      const format = property.format as Swagger.DataFormat;
-      swaggerType.description = property.description;
-      swaggerType.format = format || swaggerType.format;
-      if (!swaggerType.$ref) {
-        swaggerType.default = property.default;
-
-        Object.keys(property.validators)
-          .filter(key => {
-            return !key.startsWith('is') && key !== 'minDate' && key !== 'maxDate';
-          })
-          .forEach(key => {
-            swaggerType[key] = property.validators[key].value;
-          });
-      }
-
-      if (!property.required) {
-        swaggerType['x-nullable'] = true;
-      }
-
-      properties[property.name] = swaggerType as Swagger.Schema;
-    });
-
-    return properties;
-  }
-
-  private buildAdditionalProperties(type: Tsoa.Type) {
+  protected buildAdditionalProperties(type: Tsoa.Type) {
     return this.getSwaggerType(type);
   }
 
-  private buildOperation(controllerName: string, method: Tsoa.Method): Swagger.Operation {
+  protected buildOperation(controllerName: string, method: Tsoa.Method): Swagger.Operation {
     const swaggerResponses: any = {};
 
     method.responses.forEach((res: Tsoa.Response) => {
@@ -276,11 +31,11 @@ export class SpecGenerator {
     };
   }
 
-  private getOperationId(methodName: string) {
+  protected getOperationId(methodName: string) {
     return methodName.charAt(0).toUpperCase() + methodName.substr(1);
   }
 
-  private getSwaggerType(type: Tsoa.Type): Swagger.Schema {
+  protected getSwaggerType(type: Tsoa.Type): Swagger.Schema {
     const swaggerType = this.getSwaggerTypeForPrimitiveType(type);
     if (swaggerType) {
       return swaggerType;
@@ -297,7 +52,11 @@ export class SpecGenerator {
     return this.getSwaggerTypeForReferenceType(type as Tsoa.ReferenceType) as Swagger.Schema;
   }
 
-  private getSwaggerTypeForPrimitiveType(type: Tsoa.Type): Swagger.Schema | undefined {
+  protected getSwaggerTypeForReferenceType(referenceType: Tsoa.ReferenceType): Swagger.BaseSchema {
+    return {};
+  }
+
+  protected getSwaggerTypeForPrimitiveType(type: Tsoa.Type): Swagger.Schema | undefined {
     const map = {
       any: { type: 'object' },
       binary: { type: 'string', format: 'binary' },
@@ -317,15 +76,11 @@ export class SpecGenerator {
     return map[type.dataType];
   }
 
-  private getSwaggerTypeForArrayType(arrayType: Tsoa.ArrayType): Swagger.Schema {
+  protected getSwaggerTypeForArrayType(arrayType: Tsoa.ArrayType): Swagger.Schema {
     return { type: 'array', items: this.getSwaggerType(arrayType.elementType) };
   }
 
-  private getSwaggerTypeForEnumType(enumType: Tsoa.EnumerateType): Swagger.Schema {
+  protected getSwaggerTypeForEnumType(enumType: Tsoa.EnumerateType): Swagger.Schema {
     return { type: 'string', enum: enumType.enums.map(member => String(member)) };
   }
-
-  private getSwaggerTypeForReferenceType(referenceType: Tsoa.ReferenceType): Swagger.BaseSchema {
-    return { $ref: `#/definitions/${referenceType.refName}` };
-  }
 }
diff --git a/src/swagger/specGenerator2.ts b/src/swagger/specGenerator2.ts
new file mode 100644
index 00000000..afae2983
--- /dev/null
+++ b/src/swagger/specGenerator2.ts
@@ -0,0 +1,259 @@
+import { Tsoa } from '../metadataGeneration/tsoa';
+import { SwaggerConfig } from './../config';
+import { normalisePath } from './../utils/pathUtils';
+import { SpecGenerator } from './specGenerator';
+import { Swagger } from './swagger';
+
+export class SpecGenerator2 extends SpecGenerator {
+  constructor(protected readonly metadata: Tsoa.Metadata, protected readonly config: SwaggerConfig) {
+    super(metadata, config);
+  }
+
+  public GetSpec() {
+    let spec: Swagger.Spec2 = {
+      basePath: normalisePath(this.config.basePath as string, '/', undefined, false),
+      consumes: ['application/json'],
+      definitions: this.buildDefinitions(),
+      info: {
+        title: '',
+      },
+      paths: this.buildPaths(),
+      produces: ['application/json'],
+      swagger: '2.0',
+    };
+
+    spec.securityDefinitions = this.config.securityDefinitions
+      ? this.config.securityDefinitions
+      : {};
+
+    if (this.config.name) { spec.info.title = this.config.name; }
+    if (this.config.version) { spec.info.version = this.config.version; }
+    if (this.config.host) { spec.host = this.config.host; }
+    if (this.config.description) { spec.info.description = this.config.description; }
+    if (this.config.tags) { spec.tags = this.config.tags; }
+    if (this.config.license) { spec.info.license = { name: this.config.license }; }
+    if (this.config.spec) {
+      this.config.specMerging = this.config.specMerging || 'immediate';
+      const mergeFuncs: { [key: string]: any } = {
+        immediate: Object.assign,
+        recursive: require('merge').recursive,
+      };
+
+      spec = mergeFuncs[this.config.specMerging](spec, this.config.spec);
+    }
+    if (this.config.schemes) { spec.schemes = this.config.schemes; }
+
+    return spec;
+  }
+
+  private buildDefinitions() {
+    const definitions: { [definitionsName: string]: Swagger.Schema } = {};
+    Object.keys(this.metadata.referenceTypeMap).map(typeName => {
+      const referenceType = this.metadata.referenceTypeMap[typeName];
+
+      // Object definition
+      if (referenceType.properties) {
+        const required = referenceType.properties.filter(p => p.required).map(p => p.name);
+        definitions[referenceType.refName] = {
+          description: referenceType.description,
+          properties: this.buildProperties(referenceType.properties),
+          required: required && required.length > 0 ? Array.from(new Set(required)) : undefined,
+          type: 'object',
+        };
+
+        if (referenceType.additionalProperties) {
+          definitions[referenceType.refName].additionalProperties = this.buildAdditionalProperties(referenceType.additionalProperties);
+        }
+
+        if (referenceType.example) {
+          definitions[referenceType.refName].example = referenceType.example;
+        }
+      }
+
+      // Enum definition
+      if (referenceType.enums) {
+        definitions[referenceType.refName] = {
+          description: referenceType.description,
+          enum: referenceType.enums,
+          type: 'string',
+        };
+      }
+    });
+
+    return definitions;
+  }
+
+  private buildPaths() {
+    const paths: { [pathName: string]: Swagger.Path } = {};
+
+    this.metadata.controllers.forEach(controller => {
+      const normalisedControllerPath = normalisePath(controller.path, '/');
+      // construct documentation using all methods except @Hidden
+      controller.methods.filter(method => !method.isHidden).forEach(method => {
+        const normalisedMethodPath = normalisePath(method.path, '/');
+        const path = normalisePath(`${normalisedControllerPath}${normalisedMethodPath}`, '/', '', false);
+        paths[path] = paths[path] || {};
+        this.buildMethod(controller.name, method, paths[path]);
+      });
+    });
+
+    return paths;
+  }
+
+  private buildMethod(controllerName: string, method: Tsoa.Method, pathObject: any) {
+    const pathMethod: Swagger.Operation = pathObject[method.method] = this.buildOperation(controllerName, method);
+    pathMethod.description = method.description;
+    pathMethod.summary = method.summary;
+    pathMethod.tags = method.tags;
+
+    // Use operationId tag otherwise fallback to generated. Warning: This doesn't check uniqueness.
+    pathMethod.operationId = method.operationId || pathMethod.operationId;
+
+    if (method.deprecated) {
+      pathMethod.deprecated = method.deprecated;
+    }
+
+    if (method.security) {
+      pathMethod.security = method.security as any[];
+    }
+
+    pathMethod.parameters = method.parameters
+      .filter(p => {
+        return !(p.in === 'request' || p.in === 'body-prop');
+      })
+      .map(p => this.buildParameter(p));
+
+    const bodyPropParameter = this.buildBodyPropParameter(controllerName, method);
+    if (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 buildBodyPropParameter(controllerName: string, method: Tsoa.Method) {
+    const properties = {} as { [name: string]: Swagger.Schema };
+    const required: string[] = [];
+
+    method.parameters
+      .filter(p => p.in === 'body-prop')
+      .forEach(p => {
+        properties[p.name] = this.getSwaggerType(p.type);
+        properties[p.name].default = p.default;
+        properties[p.name].description = p.description;
+
+        if (p.required) {
+          required.push(p.name);
+        }
+      });
+
+    if (!Object.keys(properties).length) { return; }
+
+    const parameter = {
+      in: 'body',
+      name: 'body',
+      schema: {
+        properties,
+        title: `${this.getOperationId(method.name)}Body`,
+        type: 'object',
+      },
+    } as Swagger.Parameter;
+    if (required.length) {
+      parameter.schema.required = required;
+    }
+    return parameter;
+  }
+
+  private buildParameter(source: Tsoa.Parameter): Swagger.Parameter {
+    let parameter = {
+      default: source.default,
+      description: source.description,
+      in: source.in,
+      name: source.name,
+      required: source.required,
+    } as Swagger.Parameter;
+
+    const parameterType = this.getSwaggerType(source.type);
+    parameter.format = parameterType.format || undefined;
+
+    if (parameter.in === 'query' && parameterType.type === 'array') {
+      (parameter as Swagger.QueryParameter).collectionFormat = 'multi';
+    }
+
+    if (parameterType.$ref) {
+      parameter.schema = parameterType as Swagger.Schema;
+      return parameter;
+    }
+
+    const validatorObjs = {};
+    Object.keys(source.validators)
+      .filter(key => {
+        return !key.startsWith('is') && key !== 'minDate' && key !== 'maxDate';
+      })
+      .forEach((key: string) => {
+        validatorObjs[key] = source.validators[key].value;
+      });
+
+    if (source.in === 'body' && source.type.dataType === 'array') {
+      parameter.schema = {
+        items: parameterType.items,
+        type: 'array',
+      };
+    } else {
+      if (source.type.dataType === 'any') {
+        if (source.in === 'body') {
+          parameter.schema = { type: 'object' };
+        } else {
+          parameter.type = 'string';
+        }
+      } else {
+        parameter.type = parameterType.type;
+        parameter.items = parameterType.items;
+        parameter.enum = parameterType.enum;
+      }
+    }
+
+    if (parameter.schema) {
+      parameter.schema = Object.assign({}, parameter.schema, validatorObjs);
+    } else {
+      parameter = Object.assign({}, parameter, validatorObjs);
+    }
+
+    return parameter;
+  }
+
+  private buildProperties(source: Tsoa.Property[]) {
+    const properties: { [propertyName: string]: Swagger.Schema } = {};
+
+    source.forEach(property => {
+      const swaggerType = this.getSwaggerType(property.type);
+      const format = property.format as Swagger.DataFormat;
+      swaggerType.description = property.description;
+      swaggerType.format = format || swaggerType.format;
+      if (!swaggerType.$ref) {
+        swaggerType.default = property.default;
+
+        Object.keys(property.validators)
+          .filter(key => {
+            return !key.startsWith('is') && key !== 'minDate' && key !== 'maxDate';
+          })
+          .forEach(key => {
+            swaggerType[key] = property.validators[key].value;
+          });
+      }
+
+      if (!property.required) {
+        swaggerType['x-nullable'] = true;
+      }
+
+      properties[property.name] = swaggerType as Swagger.Schema;
+    });
+
+    return properties;
+  }
+
+  protected getSwaggerTypeForReferenceType(referenceType: Tsoa.ReferenceType): Swagger.BaseSchema {
+    return { $ref: `#/definitions/${referenceType.refName}` };
+  }
+}
diff --git a/src/swagger/specGenerator3.ts b/src/swagger/specGenerator3.ts
new file mode 100644
index 00000000..d3d182f8
--- /dev/null
+++ b/src/swagger/specGenerator3.ts
@@ -0,0 +1,286 @@
+import { Tsoa } from '../metadataGeneration/tsoa';
+import { SwaggerConfig } from './../config';
+import { normalisePath } from './../utils/pathUtils';
+import { SpecGenerator } from './specGenerator';
+import { Swagger } from './swagger';
+
+/**
+ * TODO:
+ * Handle formData parameters
+ * Handle tags
+ * Handle requestBodies of type other than json
+ * Handle requestBodies as reusable objects
+ * Handle headers, examples, responses, etc.
+ * Cleaner interface between SpecGenerator2 and SpecGenerator3
+ * Also accept OpenAPI 3.0.0 metadata, like components/securitySchemes instead of securityDefinitions
+ */
+export class SpecGenerator3 extends SpecGenerator {
+  constructor(protected readonly metadata: Tsoa.Metadata, protected readonly config: SwaggerConfig) {
+    super(metadata, config);
+  }
+
+  public GetSpec() {
+    let spec: Swagger.Spec3 = {
+      components: this.buildComponents(),
+      info: this.buildInfo(),
+      openapi: '3.0.0',
+      paths: this.buildPaths(),
+      servers: this.buildServers(),
+    };
+
+    if (this.config.spec) {
+      this.config.specMerging = this.config.specMerging || 'immediate';
+      const mergeFuncs: { [key: string]: any } = {
+        immediate: Object.assign,
+        recursive: require('merge').recursive,
+      };
+
+      spec = mergeFuncs[this.config.specMerging](spec, this.config.spec);
+    }
+
+    return spec;
+  }
+
+  private buildInfo() {
+    const info: Swagger.Info = {
+      title: this.config.name || '',
+    };
+    if (this.config.version) { info.version = this.config.version; }
+    if (this.config.description) { info.description = this.config.description; }
+    if (this.config.license) { info.license = { name: this.config.license }; }
+    return info;
+  }
+
+  private buildComponents() {
+    const components = {
+      examples: {},
+      headers: {},
+      parameters: {},
+      requestBodies: {},
+      responses: {},
+      schemas: this.buildSchema(),
+      securitySchemes: {},
+    };
+
+    if (this.config.securityDefinitions) {
+      components.securitySchemes = this.translateSecurityDefinitions(this.config.securityDefinitions);
+    }
+
+    return components;
+  }
+
+  private translateSecurityDefinitions(definitions: { [name: string]: Swagger.Security }) {
+    const defs: { [name: string]: Swagger.Security } = {};
+    Object.keys(definitions).forEach(key => {
+      if (definitions[key].type === 'basic') {
+        defs[key] = {
+          scheme: 'basic',
+          type: 'http',
+        } as Swagger.BasicSecurity3;
+      } else {
+        defs[key] = definitions[key];
+      }
+    });
+    return defs;
+  }
+
+  private buildServers() {
+    const basePath = normalisePath(this.config.basePath as string, '/', undefined, false);
+    const scheme = this.config.schemes ? this.config.schemes[0] : 'https';
+    const host = this.config.host || 'localhost:3000';
+    return [{
+      url: `${scheme}://${host}${basePath}`,
+    } as Swagger.Server];
+  }
+
+  private buildSchema() {
+    const schema: { [name: string]: Swagger.Schema } = {};
+    Object.keys(this.metadata.referenceTypeMap).map(typeName => {
+      const referenceType = this.metadata.referenceTypeMap[typeName];
+
+      // Object definition
+      if (referenceType.properties) {
+        const required = referenceType.properties.filter(p => p.required).map(p => p.name);
+        schema[referenceType.refName] = {
+          description: referenceType.description,
+          properties: this.buildProperties(referenceType.properties),
+          required: required && required.length > 0 ? Array.from(new Set(required)) : undefined,
+          type: 'object',
+        };
+
+        if (referenceType.additionalProperties) {
+          schema[referenceType.refName].additionalProperties = this.buildAdditionalProperties(referenceType.additionalProperties);
+        }
+
+        if (referenceType.example) {
+          schema[referenceType.refName].example = referenceType.example;
+        }
+      }
+
+      // Enum definition
+      if (referenceType.enums) {
+        schema[referenceType.refName] = {
+          description: referenceType.description,
+          enum: referenceType.enums,
+          type: 'string',
+        };
+      }
+    });
+
+    return schema;
+  }
+
+  private buildPaths() {
+    const paths: { [pathName: string]: Swagger.Path3 } = {};
+
+    this.metadata.controllers.forEach(controller => {
+      const normalisedControllerPath = normalisePath(controller.path, '/');
+      // construct documentation using all methods except @Hidden
+      controller.methods.filter(method => !method.isHidden).forEach(method => {
+        const normalisedMethodPath = normalisePath(method.path, '/');
+        const path = normalisePath(`${normalisedControllerPath}${normalisedMethodPath}`, '/', '', false);
+        paths[path] = paths[path] || {};
+        this.buildMethod(controller.name, method, paths[path]);
+      });
+    });
+
+    return paths;
+  }
+
+  private buildMethod(controllerName: string, method: Tsoa.Method, pathObject: any) {
+    const pathMethod: Swagger.Operation3 = pathObject[method.method] = this.buildOperation(controllerName, method);
+    pathMethod.description = method.description;
+    pathMethod.summary = method.summary;
+    pathMethod.tags = method.tags;
+
+    // Use operationId tag otherwise fallback to generated. Warning: This doesn't check uniqueness.
+    pathMethod.operationId = method.operationId || pathMethod.operationId;
+
+    if (method.deprecated) {
+      pathMethod.deprecated = method.deprecated;
+    }
+
+    if (method.security) {
+      pathMethod.security = method.security as any[];
+    }
+
+    const bodyParams = method.parameters.filter(p => p.in === 'body');
+
+    pathMethod.parameters = method.parameters
+      .filter(p => {
+        return ['body', 'formData', 'request', 'body-prop'].indexOf(p.in) === -1;
+      })
+      .map(p => this.buildParameter(p));
+
+    if (bodyParams.length > 1) {
+      throw new Error('Only one body parameter allowed per controller method.');
+    }
+
+    if (bodyParams.length > 0) {
+      pathMethod.requestBody = this.buildRequestBody(controllerName, method, bodyParams[0]);
+    }
+  }
+
+  private buildRequestBody(controllerName: string, method: Tsoa.Method, parameter: Tsoa.Parameter): Swagger.RequestBody {
+    const parameterType = this.getSwaggerType(parameter.type);
+
+    let schema: Swagger.Schema = { type: 'object' };
+    if (parameterType.$ref) {
+      schema = parameterType as Swagger.Schema;
+    }
+
+    if (parameter.type.dataType === 'array') {
+      schema = {
+        items: parameterType.items,
+        type: 'array',
+      };
+    }
+
+    return {
+      content: {
+        'application/json': {schema} as Swagger.MediaType,
+      },
+    } as Swagger.RequestBody;
+  }
+
+  private buildParameter(source: Tsoa.Parameter): Swagger.Parameter {
+    let parameter = {
+      default: source.default,
+      description: source.description,
+      in: source.in,
+      name: source.name,
+      required: source.required,
+    } as Swagger.Parameter;
+
+    const parameterType = this.getSwaggerType(source.type);
+    parameter.format = parameterType.format || undefined;
+
+    if (parameter.in === 'query' && parameterType.type === 'array') {
+      (parameter as Swagger.QueryParameter).collectionFormat = 'multi';
+    }
+
+    if (parameterType.$ref) {
+      parameter.schema = parameterType as Swagger.Schema;
+      return parameter;
+    }
+
+    const validatorObjs = {};
+    Object.keys(source.validators)
+      .filter(key => {
+        return !key.startsWith('is') && key !== 'minDate' && key !== 'maxDate';
+      })
+      .forEach((key: string) => {
+        validatorObjs[key] = source.validators[key].value;
+      });
+
+    if (source.type.dataType === 'any') {
+      parameter.type = 'string';
+    } else {
+      parameter.type = parameterType.type;
+      parameter.items = parameterType.items;
+      parameter.enum = parameterType.enum;
+    }
+
+    if (parameter.schema) {
+      parameter.schema = Object.assign({}, parameter.schema, validatorObjs);
+    } else {
+      parameter = Object.assign({}, parameter, validatorObjs);
+    }
+
+    return parameter;
+  }
+
+  private buildProperties(source: Tsoa.Property[]) {
+    const properties: { [propertyName: string]: Swagger.Schema3 } = {};
+
+    source.forEach(property => {
+      const swaggerType = this.getSwaggerType(property.type) as Swagger.Schema3;
+      const format = property.format as Swagger.DataFormat;
+      swaggerType.description = property.description;
+      swaggerType.format = format || swaggerType.format;
+      if (!swaggerType.$ref) {
+        swaggerType.default = property.default;
+
+        Object.keys(property.validators)
+          .filter(key => {
+            return !key.startsWith('is') && key !== 'minDate' && key !== 'maxDate';
+          })
+          .forEach(key => {
+            swaggerType[key] = property.validators[key].value;
+          });
+      }
+
+      if (!property.required) {
+        swaggerType.nullable = true;
+      }
+
+      properties[property.name] = swaggerType as Swagger.Schema;
+    });
+
+    return properties;
+  }
+
+  protected getSwaggerTypeForReferenceType(referenceType: Tsoa.ReferenceType): Swagger.BaseSchema {
+    return { $ref: `#/components/schemas/${referenceType.refName}` };
+  }
+}
diff --git a/src/swagger/swagger.ts b/src/swagger/swagger.ts
index e21d04db..7dd25be7 100644
--- a/src/swagger/swagger.ts
+++ b/src/swagger/swagger.ts
@@ -22,8 +22,13 @@ export namespace Swagger {
     | 'wss';
 
   export interface Spec {
-    swagger: '2.0';
     info: Info;
+    tags?: Tag[];
+    externalDocs?: ExternalDocs;
+  }
+
+  export interface Spec2 extends Spec {
+    swagger: '2.0';
     host?: string;
     basePath?: string;
     schemes?: Protocol[];
@@ -35,8 +40,29 @@ export namespace Swagger {
     responses?: { [name: string]: Response };
     security?: Security[];
     securityDefinitions?: { [name: string]: Security };
-    tags?: Tag[];
-    externalDocs?: ExternalDocs;
+  }
+
+  export interface Spec3 extends Spec {
+    openapi: '3.0.0';
+    servers: Server[];
+    components: Components;
+    paths: { [name: string]: Path3 };
+  }
+
+  export interface Components {
+    callbacks?: { [name: string]: any };
+    examples?: { [name: string]: any };
+    headers?: { [name: string]: any };
+    links?: { [name: string]: any };
+    parameters?: { [name: string]: Parameter };
+    requestBodies?: { [name: string]: any };
+    responses?: { [name: string]: Response };
+    schemas?: { [name: string]: any };
+    securitySchemes?: { [name: string]: Security };
+  }
+
+  export interface Server {
+    url: string;
   }
 
   export interface Info {
@@ -125,6 +151,18 @@ export namespace Swagger {
     parameters?: Parameter[];
   }
 
+  export interface Path3 {
+    $ref?: string;
+    get?: Operation3;
+    put?: Operation3;
+    post?: Operation3;
+    delete?: Operation3;
+    options?: Operation3;
+    head?: Operation3;
+    patch?: Operation3;
+    parameters?: Parameter[];
+  }
+
   export interface Operation {
     tags?: string[];
     summary?: string;
@@ -140,6 +178,35 @@ export namespace Swagger {
     security?: Security[];
   }
 
+  export interface Operation3 {
+    tags?: string[];
+    summary?: string;
+    description?: string;
+    externalDocs?: ExternalDocs;
+    operationId: string;
+    consumes?: string[];
+    produces?: string[];
+    parameters?: Parameter[];
+    responses: { [name: string]: Response };
+    schemes?: Protocol[];
+    deprecated?: boolean;
+    security?: Security[];
+    requestBody?: RequestBody;
+  }
+
+  export interface RequestBody {
+    content: { [name: string]: MediaType };
+    description?: string;
+    required?: boolean;
+  }
+
+  export interface MediaType {
+    schema?: Schema;
+    example?: { [name: string]: any };
+    examples?: { [name: string]: any };
+    encoding?: { [name: string]: any };
+  }
+
   export interface Response {
     description: string;
     schema?: Schema;
@@ -171,6 +238,10 @@ export namespace Swagger {
     items?: BaseSchema;
   }
 
+  export interface Schema3 extends Schema {
+    nullable?: boolean;
+  }
+
   export interface Schema extends BaseSchema {
     type: DataType;
     format?: DataFormat;
@@ -197,6 +268,12 @@ export namespace Swagger {
     wrapped?: boolean;
   }
 
+  export interface BasicSecurity3 {
+    type: 'http';
+    scheme: 'basic';
+    description?: string;
+  }
+
   export interface BasicSecurity {
     type: 'basic';
     description?: string;
@@ -246,6 +323,7 @@ export namespace Swagger {
   }
 
   export type Security = BasicSecurity
+    | BasicSecurity3
     | ApiKeySecurity
     | OAuth2AccessCodeSecurity
     | OAuth2ApplicationSecurity


Test patch:
diff --git a/tests/fixtures/defaultOptions.ts b/tests/fixtures/defaultOptions.ts
index 4a2ee5fe..70ef9b15 100644
--- a/tests/fixtures/defaultOptions.ts
+++ b/tests/fixtures/defaultOptions.ts
@@ -1,13 +1,18 @@
 import { SwaggerConfig } from './../../src/config';
 export function getDefaultOptions(): SwaggerConfig {
   return {
-    basePath: '/',
+    basePath: '/v1',
     description: 'Description of a test API',
     entryFile: '',
     host: 'localhost:3000',
     license: 'MIT',
     name: 'Test API',
     outputDirectory: '',
+    securityDefinitions: {
+      basic: {
+        type: 'basic',
+      },
+    },
     version: '1.0.0',
   };
 }
diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
index dad694e1..3abc7eb1 100644
--- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
+++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts
@@ -1,13 +1,13 @@
 import { expect } from 'chai';
 import 'mocha';
 import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator';
-import { SpecGenerator } from '../../../../src/swagger/specGenerator';
+import { SpecGenerator2 } from '../../../../src/swagger/specGenerator2';
 import { Swagger } from '../../../../src/swagger/swagger';
 import { getDefaultOptions } from '../../../fixtures/defaultOptions';
 
 describe('Definition generation', () => {
   const metadata = new MetadataGenerator('./tests/fixtures/controllers/getController.ts').Generate();
-  const spec = new SpecGenerator(metadata, getDefaultOptions()).GetSpec();
+  const spec = new SpecGenerator2(metadata, getDefaultOptions()).GetSpec();
 
   const getValidatedDefinition = (name: string) => {
     if (!spec.definitions) {
diff --git a/tests/unit/swagger/pathGeneration/deleteRoutes.spec.ts b/tests/unit/swagger/pathGeneration/deleteRoutes.spec.ts
index ab9835cf..56c789ea 100644
--- a/tests/unit/swagger/pathGeneration/deleteRoutes.spec.ts
+++ b/tests/unit/swagger/pathGeneration/deleteRoutes.spec.ts
@@ -1,13 +1,13 @@
 import 'mocha';
 import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator';
-import { SpecGenerator } from '../../../../src/swagger/specGenerator';
+import { SpecGenerator2 } from '../../../../src/swagger/specGenerator2';
 import { getDefaultOptions } from '../../../fixtures/defaultOptions';
 import { VerifyPathableParameter } from '../../utilities/verifyParameter';
 import { VerifyPath } from '../../utilities/verifyPath';
 
 describe('DELETE route generation', () => {
   const metadata = new MetadataGenerator('./tests/fixtures/controllers/deleteController.ts').Generate();
-  const spec = new SpecGenerator(metadata, getDefaultOptions()).GetSpec();
+  const spec = new SpecGenerator2(metadata, getDefaultOptions()).GetSpec();
   const baseRoute = '/DeleteTest';
 
   it('should generate a path for a DELETE route with no path argument', () => {
diff --git a/tests/unit/swagger/pathGeneration/getRoutes.spec.ts b/tests/unit/swagger/pathGeneration/getRoutes.spec.ts
index 35a6c7e4..1a33aeea 100644
--- a/tests/unit/swagger/pathGeneration/getRoutes.spec.ts
+++ b/tests/unit/swagger/pathGeneration/getRoutes.spec.ts
@@ -1,14 +1,14 @@
 import { expect } from 'chai';
 import 'mocha';
 import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator';
-import { SpecGenerator } from '../../../../src/swagger/specGenerator';
+import { SpecGenerator2 } from '../../../../src/swagger/specGenerator2';
 import { getDefaultOptions } from '../../../fixtures/defaultOptions';
 import { VerifyPathableNumberParameter, VerifyPathableParameter, VerifyPathableStringParameter } from '../../utilities/verifyParameter';
 import { VerifyPath } from '../../utilities/verifyPath';
 
 describe('GET route generation', () => {
   const metadata = new MetadataGenerator('./tests/fixtures/controllers/getController.ts').Generate();
-  const spec = new SpecGenerator(metadata, getDefaultOptions()).GetSpec();
+  const spec = new SpecGenerator2(metadata, getDefaultOptions()).GetSpec();
   const baseRoute = '/GetTest';
 
   const getValidatedGetOperation = (actionRoute: string) => {
@@ -63,7 +63,7 @@ describe('GET route generation', () => {
 
   it('should generate a path for a GET route with no controller path argument', () => {
     const pathlessMetadata = new MetadataGenerator('./tests/fixtures/controllers/pathlessGetController.ts').Generate();
-    const pathlessSpec = new SpecGenerator(pathlessMetadata, getDefaultOptions()).GetSpec();
+    const pathlessSpec = new SpecGenerator2(pathlessMetadata, getDefaultOptions()).GetSpec();
     VerifyPath(pathlessSpec, '/Current', (path) => path.get, false);
   });
 
@@ -140,7 +140,7 @@ describe('GET route generation', () => {
   it('should reject complex types as arguments', () => {
     expect(() => {
       const invalidMetadata = new MetadataGenerator('./tests/fixtures/controllers/invalidGetController.ts').Generate();
-      new SpecGenerator(invalidMetadata, getDefaultOptions()).GetSpec();
+      new SpecGenerator2(invalidMetadata, getDefaultOptions()).GetSpec();
     }).to.throw('@Query(\'myModel\') Can\'t support \'refObject\' type. \n in \'InvalidGetTestController.getModelWithComplex\'');
   });
 
diff --git a/tests/unit/swagger/pathGeneration/headRoutes.spec.ts b/tests/unit/swagger/pathGeneration/headRoutes.spec.ts
index c3d543f9..4f00d2d3 100644
--- a/tests/unit/swagger/pathGeneration/headRoutes.spec.ts
+++ b/tests/unit/swagger/pathGeneration/headRoutes.spec.ts
@@ -1,13 +1,13 @@
 import 'mocha';
 import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator';
-import { SpecGenerator } from '../../../../src/swagger/specGenerator';
+import { SpecGenerator2 } from '../../../../src/swagger/specGenerator2';
 import { getDefaultOptions } from '../../../fixtures/defaultOptions';
 import { VerifyPathableParameter } from '../../utilities/verifyParameter';
 import { VerifyPath } from '../../utilities/verifyPath';
 
 describe('HEAD route generation', () => {
   const metadata = new MetadataGenerator('./tests/fixtures/controllers/headController.ts').Generate();
-  const spec = new SpecGenerator(metadata, getDefaultOptions()).GetSpec();
+  const spec = new SpecGenerator2(metadata, getDefaultOptions()).GetSpec();
   const baseRoute = '/HeadTest';
 
   it('should generate a path for a HEAD route with no path argument', () => {
diff --git a/tests/unit/swagger/pathGeneration/patchRoutes.spec.ts b/tests/unit/swagger/pathGeneration/patchRoutes.spec.ts
index 847448b7..5af18fd5 100644
--- a/tests/unit/swagger/pathGeneration/patchRoutes.spec.ts
+++ b/tests/unit/swagger/pathGeneration/patchRoutes.spec.ts
@@ -1,13 +1,13 @@
 import 'mocha';
 import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator';
-import { SpecGenerator } from '../../../../src/swagger/specGenerator';
+import { SpecGenerator2 } from '../../../../src/swagger/specGenerator2';
 import { getDefaultOptions } from '../../../fixtures/defaultOptions';
 import { VerifyBodyParameter, VerifyPathableParameter } from '../../utilities/verifyParameter';
 import { defaultModelName, VerifyPath } from '../../utilities/verifyPath';
 
 describe('PATCH route generation', () => {
   const metadata = new MetadataGenerator('./tests/fixtures/controllers/patchController.ts').Generate();
-  const spec = new SpecGenerator(metadata, getDefaultOptions()).GetSpec();
+  const spec = new SpecGenerator2(metadata, getDefaultOptions()).GetSpec();
   const baseRoute = '/PatchTest';
 
   it('should generate a path for a PATCH route with no path argument', () => {
diff --git a/tests/unit/swagger/pathGeneration/postRoutes.spec.ts b/tests/unit/swagger/pathGeneration/postRoutes.spec.ts
index 6acc6aed..175ae282 100644
--- a/tests/unit/swagger/pathGeneration/postRoutes.spec.ts
+++ b/tests/unit/swagger/pathGeneration/postRoutes.spec.ts
@@ -1,14 +1,14 @@
 import * as chai from 'chai';
 import 'mocha';
 import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator';
-import { SpecGenerator } from '../../../../src/swagger/specGenerator';
+import { SpecGenerator2 } from '../../../../src/swagger/specGenerator2';
 import { getDefaultOptions } from '../../../fixtures/defaultOptions';
 import { VerifyBodyParameter, VerifyPathableParameter } from '../../utilities/verifyParameter';
 import { defaultModelName, VerifyPath } from '../../utilities/verifyPath';
 
 describe('POST route generation', () => {
   const metadata = new MetadataGenerator('./tests/fixtures/controllers/postController.ts').Generate();
-  const spec = new SpecGenerator(metadata, getDefaultOptions()).GetSpec();
+  const spec = new SpecGenerator2(metadata, getDefaultOptions()).GetSpec();
   const baseRoute = '/PostTest';
 
   const getValidatedParameters = (actionRoute: string) => {
@@ -47,7 +47,7 @@ describe('POST route generation', () => {
   it('should reject multiple body parameters', () => {
     chai.expect(() => {
       const invalidMetadata = new MetadataGenerator('./tests/fixtures/controllers/invalidPostController.ts').Generate();
-      new SpecGenerator(invalidMetadata, getDefaultOptions()).GetSpec();
+      new SpecGenerator2(invalidMetadata, getDefaultOptions()).GetSpec();
     }).to.throw('Only one body parameter allowed in \'InvalidPostTestController.postWithMultipleBodyParams\' method.');
   });
 
diff --git a/tests/unit/swagger/pathGeneration/putRoutes.spec.ts b/tests/unit/swagger/pathGeneration/putRoutes.spec.ts
index 367f220b..b3154490 100644
--- a/tests/unit/swagger/pathGeneration/putRoutes.spec.ts
+++ b/tests/unit/swagger/pathGeneration/putRoutes.spec.ts
@@ -1,13 +1,13 @@
 import 'mocha';
 import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator';
-import { SpecGenerator } from '../../../../src/swagger/specGenerator';
+import { SpecGenerator2 } from '../../../../src/swagger/specGenerator2';
 import { getDefaultOptions } from '../../../fixtures/defaultOptions';
 import { VerifyBodyParameter, VerifyPathableParameter } from '../../utilities/verifyParameter';
 import { defaultModelName, VerifyPath } from '../../utilities/verifyPath';
 
 describe('PUT route generation', () => {
   const metadata = new MetadataGenerator('./tests/fixtures/controllers/putController.ts').Generate();
-  const spec = new SpecGenerator(metadata, getDefaultOptions()).GetSpec();
+  const spec = new SpecGenerator2(metadata, getDefaultOptions()).GetSpec();
   const baseRoute = '/PutTest';
 
   const getValidatedParameters = (actionRoute: string) => {
diff --git a/tests/unit/swagger/pathGeneration/securityRoutes.spec.ts b/tests/unit/swagger/pathGeneration/securityRoutes.spec.ts
index f2d4200c..d41fbe54 100644
--- a/tests/unit/swagger/pathGeneration/securityRoutes.spec.ts
+++ b/tests/unit/swagger/pathGeneration/securityRoutes.spec.ts
@@ -1,13 +1,13 @@
 import { expect } from 'chai';
 import 'mocha';
 import { MetadataGenerator } from '../../../../src/metadataGeneration/metadataGenerator';
-import { SpecGenerator } from '../../../../src/swagger/specGenerator';
+import { SpecGenerator2 } from '../../../../src/swagger/specGenerator2';
 import { getDefaultOptions } from '../../../fixtures/defaultOptions';
 import { VerifyPath } from '../../utilities/verifyPath';
 
 describe('Security route generation', () => {
   const metadata = new MetadataGenerator('./tests/fixtures/controllers/securityController.ts').Generate();
-  const spec = new SpecGenerator(metadata, getDefaultOptions()).GetSpec();
+  const spec = new SpecGenerator2(metadata, getDefaultOptions()).GetSpec();
 
   it('should generate a route with a named security', () => {
     const path = verifyPath('/SecurityTest');
diff --git a/tests/unit/swagger/schemaDetails.spec.ts b/tests/unit/swagger/schemaDetails.spec.ts
index ac756e12..d291c03e 100644
--- a/tests/unit/swagger/schemaDetails.spec.ts
+++ b/tests/unit/swagger/schemaDetails.spec.ts
@@ -1,12 +1,12 @@
 import { expect } from 'chai';
 import 'mocha';
 import { MetadataGenerator } from '../../../src/metadataGeneration/metadataGenerator';
-import { SpecGenerator } from '../../../src/swagger/specGenerator';
+import { SpecGenerator2 } from '../../../src/swagger/specGenerator2';
 import { getDefaultOptions } from '../../fixtures/defaultOptions';
 
 describe('Schema details generation', () => {
   const metadata = new MetadataGenerator('./tests/fixtures/controllers/getController.ts').Generate();
-  const spec = new SpecGenerator(metadata, getDefaultOptions()).GetSpec();
+  const spec = new SpecGenerator2(metadata, getDefaultOptions()).GetSpec();
 
   if (!spec.info) { throw new Error('No spec info.'); }
   if (!spec.info.title) { throw new Error('No spec info title.'); }
diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts
new file mode 100644
index 00000000..8649990c
--- /dev/null
+++ b/tests/unit/swagger/schemaDetails3.spec.ts
@@ -0,0 +1,91 @@
+import { expect } from 'chai';
+import 'mocha';
+import { MetadataGenerator } from '../../../src/metadataGeneration/metadataGenerator';
+import { SpecGenerator3 } from '../../../src/swagger/specGenerator3';
+import { Swagger } from '../../../src/swagger/swagger';
+import { getDefaultOptions } from '../../fixtures/defaultOptions';
+
+describe('Definition generation for OpenAPI 3.0.0', () => {
+  const metadata = new MetadataGenerator('./tests/fixtures/controllers/getController.ts').Generate();
+  const spec = new SpecGenerator3(metadata, getDefaultOptions()).GetSpec();
+
+  describe('servers', () => {
+    it('should replace the parent schemes element', () => {
+      expect(spec).to.not.have.property('schemes');
+      expect(spec.servers[0].url).to.match(/^https/);
+    });
+
+    it('should replace the parent host element', () => {
+      expect(spec).to.not.have.property('host');
+      expect(spec.servers[0].url).to.match(/localhost:3000/);
+    });
+
+    it('should replace the parent basePath element', () => {
+      expect(spec).to.not.have.property('basePath');
+      expect(spec.servers[0].url).to.match(/\/v1/);
+    });
+  });
+
+  describe('security', () => {
+    it('should replace the parent securityDefinitions with securitySchemes within components', () => {
+      expect(spec).to.not.have.property('securityDefinitions');
+      expect(spec.components.securitySchemes).to.be.ok;
+    });
+
+    it('should replace type: basic with type: http and scheme: basic', () => {
+      if (!spec.components.securitySchemes) { throw new Error('No security schemes.'); }
+      if (!spec.components.securitySchemes.basic) { throw new Error('No basic security scheme.'); }
+
+      const basic = spec.components.securitySchemes.basic as Swagger.BasicSecurity3;
+
+      expect(basic.type).to.equal('http');
+      expect(basic.scheme).to.equal('basic');
+    });
+  });
+
+  describe('paths', () => {
+    describe('requestBody', () => {
+      it('should replace the body parameter with a requestBody', () => {
+        const metadataPost = new MetadataGenerator('./tests/fixtures/controllers/postController.ts').Generate();
+        const specPost = new SpecGenerator3(metadataPost, getDefaultOptions()).GetSpec();
+
+        if (!specPost.paths) { throw new Error('Paths are not defined.'); }
+        if (!specPost.paths['/PostTest']) { throw new Error('PostTest path not defined.'); }
+        if (!specPost.paths['/PostTest'].post) { throw new Error('PostTest post method not defined.'); }
+
+        const method = specPost.paths['/PostTest'].post;
+
+        if (!method || !method.parameters) { throw new Error('Parameters not defined.'); }
+
+        expect(method.parameters).to.deep.equal([]);
+
+        if (!method.requestBody) { throw new Error('Request body not defined.'); }
+
+        expect(method.requestBody.content['application/json'].schema).to.deep.equal({
+          '$ref': '#/components/schemas/TestModel'
+        });
+      });
+    });
+  });
+
+  describe('components', () => {
+    describe('schemas', () => {
+      it('should replace definitions with schemas', () => {
+        if (!spec.components.schemas) { throw new Error('Schemas not defined.'); }
+
+        expect(spec).to.not.have.property('definitions');
+        expect(spec.components.schemas.TestModel).to.exist;
+      });
+
+      it('should replace x-nullable with nullable', () => {
+        if (!spec.components.schemas) { throw new Error('Schemas not defined.'); }
+        if (!spec.components.schemas.TestModel) { throw new Error('TestModel not defined.'); }
+
+        const testModel = spec.components.schemas.TestModel;
+
+        expect(testModel.properties.optionalString).to.not.have.property('x-nullable');
+        expect(testModel.properties.optionalString.nullable).to.be.true;
+      });
+    });
+  });
+});
diff --git a/tests/unit/utilities/verifyPath.ts b/tests/unit/utilities/verifyPath.ts
index faa3783b..15358959 100644
--- a/tests/unit/utilities/verifyPath.ts
+++ b/tests/unit/utilities/verifyPath.ts
@@ -4,7 +4,7 @@ import { Swagger } from '../../../src/swagger/swagger';
 export const defaultModelName = '#/definitions/TestModel';
 
 export function VerifyPath(
-  spec: Swagger.Spec,
+  spec: Swagger.Spec2,
   route: string,
   getOperation: ((path: Swagger.Path) => Swagger.Operation | undefined),
   isCollection?: boolean,