Repository: tipsy/javalin
Pull request #576 (2019-05-11 08:00:16)

[openapi] Add OpenAPI schema generation by dsl (#254)
This PR contains the core and the DSL of the open api schema generation.
The annotation api will be a separate PR.

For more details please refer to the original PR (#568)

tipsy: Could you put everything in `plugin.openapi.dsl`, then `plugin.openapi.annotations` for the other PR ?
tipsy: @TobiasWalle is everything in `src/test/java/io/javalin/openapi/json.kt` required, or is there a lot of repetition? Would it be possible to cut it down a bit? Maybe we can get rid of `src/test/resources/exampleApiSpec.yaml` now too?
TobiasWalle: > Could you put everything in `plugin.openapi.dsl`, then `plugin.openapi.annotations` for the other PR ?

I'm not sure. The idea of the packages is:

io.javalin.plugin.openapi -> Static methods to get the schema and the handler
io.javalin.plugin.openapi.dsl -> Methods to *define* the schema with code
io.javalin.plugin.openapi.annotation (next PR) -> Methods to *define* the schema with annotations

io.javalin.plugin.openapi.external (internal, not for public use) -> Extention of external libraries. The long term strategy is to merge my changes into the specific library and remove this. I created issues on the library repo for this.

I moved the "utils" package into the dsl package, as this makes more sense.

Is this okay, or do you want to merge the other two packages into the "dsl" package?
TobiasWalle: > @TobiasWalle is everything in `src/test/java/io/javalin/openapi/json.kt` required, or is there a lot of repetition? Would it be possible to cut it down a bit? Maybe we can get rid of `src/test/resources/exampleApiSpec.yaml` now too?

Unfortunately most of it is required. Here an overview:

Complex Example (should cover all the use cases):
GET "/unimplemented" -> Example for documentation of path outside javalin
GET "/user" -> Example of json response and generated schema
PUT "/user" -> Example of another method than GET and different requestBodies
GET "/users" -> Example of parameters and an array json response with the "isArray" option
GET "/users2" -> Example of an array json response by reflection "Array<User>"
GET "/string" -> Example of string response
GET "/homepage" -> Example of html response
GET "/upload" -> Example of file upload with one file
GET "/uploads" -> Example of file upload with several file
GET "/resources/*" -> Example of wild cards

Crud Example (required for Crud Test)

ProvideRoute Example (This could be removed, but I didn't want to use the complex example for testing just the route. This example should be very basic)

If I would remove any of the routes the test coverage would take a hit.
TobiasWalle: > Maybe we can get rid of `src/test/resources/exampleApiSpec.yaml` now too?

The example is version "2.0" and I generate the "3.0" specification. 

I plan to provide the schema as in a swagger ui directly, but I wanted to do this as a separate PR, as this one is already big enough. Upgrading the version would be a major change.
tipsy: > Is this okay, or do you want to merge the other two packages into the "dsl" package?

That sounds fine. When you said earlier that the annotations would use the dsl, it made sense for me to split them into those two packages, but your setup makes sense.

> I plan to provide the schema as in a swagger ui directly, but I wanted to do this as a separate PR, as this one is already big enough. Upgrading the version would be a major change.

Perfect!
TobiasWalle: All comments should be resolved
tipsy: I'd be lying if I said I had a good overview of the PR, but since it's a plugin I'll trust you if you think it's ready for merging. Then I can play around with it locally over the next few days/weeks to understand it better.
TobiasWalle: > I'd be lying if I said I had a good overview of the PR, but since it's a plugin I'll trust you if you think it's ready for merging. Then I can play around with it locally over the next few days/weeks to understand it better.

I think it is ready to merge. If you find something in the following days/weeks just let me know and I will fix it.
tipsy: Great, thanks again!

Patch:
diff --git a/pom.xml b/pom.xml
index 5a123ca6..b725f764 100644
--- a/pom.xml
+++ b/pom.xml
@@ -154,6 +154,18 @@
             <version>1.1.3</version>
             <optional>true</optional>
         </dependency>
+        <dependency>
+            <groupId>io.swagger.core.v3</groupId>
+            <artifactId>swagger-models</artifactId>
+            <version>2.0.7</version>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>cc.vileda</groupId>
+            <artifactId>kotlin-openapi3-dsl</artifactId>
+            <version>0.20.1</version>
+            <optional>true</optional>
+        </dependency>
         <!-- END Optional dependencies -->
 
         <!-- BEGIN Test dependencies -->
diff --git a/src/main/java/io/javalin/Javalin.java b/src/main/java/io/javalin/Javalin.java
index bd58cc9e..8c79d671 100644
--- a/src/main/java/io/javalin/Javalin.java
+++ b/src/main/java/io/javalin/Javalin.java
@@ -45,7 +45,7 @@ public class Javalin {
 
     public static Logger log = LoggerFactory.getLogger(Javalin.class);
 
-    protected JavalinConfig config = new JavalinConfig();
+    public JavalinConfig config = new JavalinConfig();
 
     protected JavalinServer server; // null in standalone-mode
     protected JavalinWsServlet wsServlet; // null in standalone-mode
diff --git a/src/main/java/io/javalin/apibuilder/ApiBuilder.java b/src/main/java/io/javalin/apibuilder/ApiBuilder.java
index d3ad0b62..84b463c5 100644
--- a/src/main/java/io/javalin/apibuilder/ApiBuilder.java
+++ b/src/main/java/io/javalin/apibuilder/ApiBuilder.java
@@ -11,10 +11,12 @@ import io.javalin.core.security.AccessManager;
 import io.javalin.core.security.Role;
 import io.javalin.http.Handler;
 import io.javalin.http.sse.SseClient;
+import io.javalin.plugin.openapi.dsl.OpenApiBuilder;
 import io.javalin.websocket.WsHandler;
 import java.util.ArrayDeque;
 import java.util.Deque;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
 import org.jetbrains.annotations.NotNull;
@@ -486,11 +488,14 @@ public class ApiBuilder {
         String SEPARATOR = "/:";
         String resourceBase = path.substring(0, path.lastIndexOf(SEPARATOR));
         String resourceId = path.substring(path.lastIndexOf(SEPARATOR) + SEPARATOR.length());
-        staticInstance().get(prefixPath(path), ctx -> crudHandler.getOne(ctx, ctx.pathParam(resourceId)), permittedRoles);
-        staticInstance().get(prefixPath(resourceBase), crudHandler::getAll, permittedRoles);
-        staticInstance().post(prefixPath(resourceBase), crudHandler::create, permittedRoles);
-        staticInstance().patch(prefixPath(path), ctx -> crudHandler.update(ctx, ctx.pathParam(resourceId)), permittedRoles);
-        staticInstance().delete(prefixPath(path), ctx -> crudHandler.delete(ctx, ctx.pathParam(resourceId)), permittedRoles);
-    }
 
+        Map<CrudHandlerLambdaKey, Handler> lambdas = CrudHandlerKt.getLambdas(crudHandler, resourceId);
+        lambdas = OpenApiBuilder.documented(crudHandler, lambdas);
+
+        staticInstance().get(prefixPath(path), lambdas.get(CrudHandlerLambdaKey.GET_ONE), permittedRoles);
+        staticInstance().get(prefixPath(resourceBase), lambdas.get(CrudHandlerLambdaKey.GET_ALL), permittedRoles);
+        staticInstance().post(prefixPath(resourceBase), lambdas.get(CrudHandlerLambdaKey.CREATE), permittedRoles);
+        staticInstance().patch(prefixPath(path), lambdas.get(CrudHandlerLambdaKey.UPDATE), permittedRoles);
+        staticInstance().delete(prefixPath(path), lambdas.get(CrudHandlerLambdaKey.DELETE), permittedRoles);
+    }
 }
diff --git a/src/main/java/io/javalin/apibuilder/CrudHandler.kt b/src/main/java/io/javalin/apibuilder/CrudHandler.kt
index 5470484d..a5cc98f8 100644
--- a/src/main/java/io/javalin/apibuilder/CrudHandler.kt
+++ b/src/main/java/io/javalin/apibuilder/CrudHandler.kt
@@ -7,6 +7,7 @@
 package io.javalin.apibuilder
 
 import io.javalin.http.Context
+import io.javalin.http.Handler
 
 /**
  * The CrudHandler is an interface for handling the five most
@@ -21,3 +22,22 @@ interface CrudHandler {
     fun update(ctx: Context, resourceId: String)
     fun delete(ctx: Context, resourceId: String)
 }
+
+internal enum class CrudHandlerLambdaKey(val value: String) {
+    GET_ALL("getAll"),
+    GET_ONE("getOne"),
+    CREATE("create"),
+    UPDATE("update"),
+    DELETE("delete")
+}
+
+internal fun CrudHandler.getLambdas(resourceId: String): Map<CrudHandlerLambdaKey, Handler> {
+    val crudHandler = this
+    return mapOf(
+            CrudHandlerLambdaKey.GET_ALL to Handler { ctx -> crudHandler.getAll(ctx) },
+            CrudHandlerLambdaKey.GET_ONE to Handler { ctx -> crudHandler.getOne(ctx, ctx.pathParam(resourceId)) },
+            CrudHandlerLambdaKey.CREATE to Handler { ctx -> crudHandler.create(ctx) },
+            CrudHandlerLambdaKey.UPDATE to Handler { ctx -> crudHandler.update(ctx, ctx.pathParam(resourceId)) },
+            CrudHandlerLambdaKey.DELETE to Handler { ctx -> crudHandler.delete(ctx, ctx.pathParam(resourceId)) }
+    )
+}
diff --git a/src/main/java/io/javalin/core/JavalinConfig.java b/src/main/java/io/javalin/core/JavalinConfig.java
index 535eeb72..29e082a3 100644
--- a/src/main/java/io/javalin/core/JavalinConfig.java
+++ b/src/main/java/io/javalin/core/JavalinConfig.java
@@ -24,6 +24,8 @@ import io.javalin.http.util.CorsBeforeHandler;
 import io.javalin.http.util.CorsOptionsHandler;
 import io.javalin.plugin.metrics.JavalinMicrometer;
 import io.javalin.plugin.metrics.MetricsProvider;
+import io.javalin.plugin.openapi.OpenApiHandler;
+import io.javalin.plugin.openapi.OpenApiOptions;
 import io.javalin.websocket.WsHandler;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -66,6 +68,8 @@ public class JavalinConfig {
         @NotNull public Map<Class<?>, Object> appAttributes = new HashMap<>();
         @NotNull public List<String> corsOrigins = new ArrayList<>();
         @Nullable public RouteOverviewConfig routeOverview = null;
+        @Nullable public OpenApiOptions openApiOptions = null;
+        @Nullable public OpenApiHandler openApiHandler = null;
         @Nullable public RequestLogger requestLogger = null;
         @Nullable public ResourceHandler resourceHandler = null;
         @NotNull public AccessManager accessManager = SecurityUtil::noopAccessManager;
@@ -105,6 +109,15 @@ public class JavalinConfig {
         inner.routeOverview = new RouteOverviewConfig(path, permittedRoles);
     }
 
+    /**
+     * Enable the automatic generation of an open api schema.
+     * The schema can be extracted with {@link io.javalin.plugin.openapi.JavalinOpenApi#createSchema(Javalin)}.
+     */
+    public void enableOpenApi(OpenApiOptions options) {
+        inner.openApiOptions = options;
+    }
+
+
     public void accessManager(@NotNull AccessManager accessManager) {
         inner.accessManager = accessManager;
     }
@@ -134,9 +147,25 @@ public class JavalinConfig {
 
     public static void applyUserConfig(Javalin app, JavalinConfig config, Consumer<JavalinConfig> userConfig) {
         userConfig.accept(config); // apply user config to the default config
+
+        // The following handlers need to be initialized before any handler is added
+        RouteOverviewRenderer routeOverviewRenderer = null;
         if (config.inner.routeOverview != null) {
-            app.get(config.inner.routeOverview.getPath(), new RouteOverviewRenderer(app), config.inner.routeOverview.getRoles());
+            routeOverviewRenderer = new RouteOverviewRenderer(app);
+        }
+        OpenApiHandler openApiHandler = null;
+        if (config.inner.openApiOptions != null) {
+            openApiHandler = new OpenApiHandler(app, config.inner.openApiOptions);
+            config.inner.openApiHandler = openApiHandler;
         }
+
+        if (routeOverviewRenderer != null) {
+            app.get(config.inner.routeOverview.getPath(), routeOverviewRenderer, config.inner.routeOverview.getRoles());
+        }
+        if (openApiHandler != null && config.inner.openApiOptions.getPath() != null) {
+            app.get(config.inner.openApiOptions.getPath(), openApiHandler, config.inner.openApiOptions.getRoles());
+        }
+
         if (!config.inner.corsOrigins.isEmpty()) {
             app.before(new CorsBeforeHandler(config.inner.corsOrigins));
             app.options("*", new CorsOptionsHandler(), roles(CoreRoles.NO_WRAP));
diff --git a/src/main/java/io/javalin/core/PathParser.kt b/src/main/java/io/javalin/core/PathParser.kt
index 6a320f1b..c638e770 100644
--- a/src/main/java/io/javalin/core/PathParser.kt
+++ b/src/main/java/io/javalin/core/PathParser.kt
@@ -10,7 +10,7 @@ import io.javalin.http.util.ContextUtil
 
 class PathParser(path: String) {
 
-    private val segments: List<PathSegment> = path.split("/")
+    internal val segments: List<PathSegment> = path.split("/")
             .filter { it.isNotEmpty() }
             .map {
                 when {
@@ -20,7 +20,7 @@ class PathParser(path: String) {
                 }
             }
 
-    private val pathParamNames = segments.filterIsInstance<PathSegment.Parameter>().map { it.name }
+    internal val pathParamNames = segments.filterIsInstance<PathSegment.Parameter>().map { it.name }
 
     private val matchRegex = "^/${segments.joinToString("/") { it.asRegexString() }}/?$".toRegex()
 
diff --git a/src/main/java/io/javalin/plugin/openapi/CreateBaseConfiguration.java b/src/main/java/io/javalin/plugin/openapi/CreateBaseConfiguration.java
new file mode 100644
index 00000000..a47d3d2e
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/CreateBaseConfiguration.java
@@ -0,0 +1,13 @@
+package io.javalin.plugin.openapi;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Create the base open api configuration.
+ * This function will be called before the creation of every schema.
+ */
+@FunctionalInterface
+public interface CreateBaseConfiguration {
+    @NotNull OpenAPI create();
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/JavalinOpenApi.kt b/src/main/java/io/javalin/plugin/openapi/JavalinOpenApi.kt
new file mode 100644
index 00000000..219f95dd
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/JavalinOpenApi.kt
@@ -0,0 +1,56 @@
+package io.javalin.plugin.openapi
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import io.javalin.Javalin
+import io.javalin.core.event.HandlerMetaInfo
+import io.javalin.plugin.openapi.dsl.applyMetaInfoList
+import io.javalin.plugin.openapi.dsl.updateComponents
+import io.javalin.plugin.openapi.dsl.updatePaths
+import io.swagger.v3.core.converter.ModelConverters
+import io.swagger.v3.core.jackson.ModelResolver
+import io.swagger.v3.oas.models.OpenAPI
+
+class CreateSchemaOptions(
+        val objectMapper: ObjectMapper,
+        val handlerMetaInfoList: List<HandlerMetaInfo>,
+
+        /**
+         * Create the base open api configuration.
+         * This function will be called before the creation of every schema.
+         */
+        val createBaseConfiguration: CreateBaseConfiguration
+)
+
+object JavalinOpenApi {
+    fun createSchema(javalin: Javalin): OpenAPI {
+        val handler = javalin.config.inner.openApiHandler
+        return handler?.createOpenAPISchema()
+                ?: throw IllegalStateException("You need to activate the \"enableOpenApi\" option before you can create the OpenAPI schema");
+    }
+
+    fun createSchema(options: CreateSchemaOptions): OpenAPI {
+        val baseConfiguration = options.createBaseConfiguration.create()
+        return runWithObjectMapper(options.objectMapper) {
+            baseConfiguration.apply {
+                updateComponents {
+                    applyMetaInfoList(options.handlerMetaInfoList)
+                }
+                updatePaths {
+                    applyMetaInfoList(options.handlerMetaInfoList)
+                }
+            }
+        }
+    }
+}
+
+/**
+ * This function temporary sets the ModelResolve to the given object mapper.
+ * This is required, so the classes are correctly parse to OpenAPI schemas
+ */
+private fun <T> runWithObjectMapper(objectMapper: ObjectMapper, run: () -> T): T {
+    val modelResolver = ModelResolver(objectMapper)
+    ModelConverters.getInstance().addConverter(modelResolver)
+    val result = run()
+    ModelConverters.getInstance().removeConverter(modelResolver)
+    return result
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/OpenApiHandler.kt b/src/main/java/io/javalin/plugin/openapi/OpenApiHandler.kt
new file mode 100644
index 00000000..a5b22178
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/OpenApiHandler.kt
@@ -0,0 +1,26 @@
+package io.javalin.plugin.openapi
+
+import io.javalin.Javalin
+import io.javalin.core.event.HandlerMetaInfo
+import io.javalin.http.Context
+import io.javalin.http.Handler
+import io.javalin.plugin.json.JavalinJackson
+import io.swagger.v3.oas.models.OpenAPI
+
+class OpenApiHandler(app: Javalin, val options: OpenApiOptions) : Handler {
+    private val handlerMetaInfoList = mutableListOf<HandlerMetaInfo>()
+
+    init {
+        app.events { it.handlerAdded { handlerInfo -> handlerMetaInfoList.add(handlerInfo) } }
+    }
+
+    fun createOpenAPISchema(): OpenAPI = JavalinOpenApi.createSchema(CreateSchemaOptions(
+            handlerMetaInfoList = handlerMetaInfoList,
+            objectMapper = JavalinJackson.getObjectMapper(),
+            createBaseConfiguration = options.createBaseConfiguration
+    ))
+
+    override fun handle(ctx: Context) {
+        ctx.json(createOpenAPISchema())
+    }
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/OpenApiOptions.kt b/src/main/java/io/javalin/plugin/openapi/OpenApiOptions.kt
new file mode 100644
index 00000000..2964010f
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/OpenApiOptions.kt
@@ -0,0 +1,43 @@
+package io.javalin.plugin.openapi
+
+import io.javalin.core.security.Role
+import io.swagger.v3.oas.models.OpenAPI
+import io.swagger.v3.oas.models.info.Info
+
+class OpenApiOptions @JvmOverloads constructor(
+        /** Creates a GET route to get the schema as a json if not null */
+        val path: String? = null,
+        val roles: Set<Role> = setOf(),
+        val createBaseConfiguration: CreateBaseConfiguration
+) {
+    constructor(
+            createBaseConfiguration: () -> OpenAPI
+    ) : this(null, setOf(), CreateBaseConfiguration(createBaseConfiguration))
+
+    constructor(
+            path: String? = null,
+            createBaseConfiguration: () -> OpenAPI
+    ) : this(path, setOf(), CreateBaseConfiguration(createBaseConfiguration))
+
+
+    constructor(
+            path: String? = null,
+            roles: Set<Role> = setOf(),
+            createBaseConfiguration: () -> OpenAPI
+    ) : this(path, roles, CreateBaseConfiguration(createBaseConfiguration))
+
+    @JvmOverloads constructor(
+            path: String? = null,
+            info: Info
+    ) : this(path, setOf(), {
+        OpenAPI().info(info)
+    })
+
+    constructor(
+            path: String? = null,
+            roles: Set<Role> = setOf(),
+            info: Info
+    ) : this(path, roles, {
+        OpenAPI().info(info)
+    })
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedContent.kt b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedContent.kt
new file mode 100644
index 00000000..7f46f37c
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedContent.kt
@@ -0,0 +1,107 @@
+package io.javalin.plugin.openapi.dsl
+
+import io.javalin.plugin.openapi.external.mediaType
+import io.javalin.plugin.openapi.external.mediaTypeArrayOf
+import io.javalin.plugin.openapi.external.mediaTypeArrayOfRef
+import io.javalin.plugin.openapi.external.mediaTypeRef
+import io.javalin.plugin.openapi.external.schema
+import io.swagger.v3.oas.models.Components
+import io.swagger.v3.oas.models.media.Content
+import io.swagger.v3.oas.models.media.MediaType
+import io.swagger.v3.oas.models.media.Schema
+import java.math.BigDecimal
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.util.*
+
+class DocumentedContent(
+        returnType: Class<*>,
+        isArray: Boolean,
+        contentType: String? = null,
+        val schema: Schema<*>? = null
+) {
+    val contentType: String = contentType ?: returnType.guessContentType()
+
+    private val returnTypeIsArray = returnType.isArray
+
+    private val isNotByteArray = if (schema == null) {
+        returnType != ByteArray::class.java
+    } else {
+        schema.type == "string" && schema.format == "application/octet-stream"
+    }
+
+    val returnType = when {
+        returnTypeIsArray && isNotByteArray -> returnType.componentType!!
+        else -> returnType
+    }
+
+    val isArray = if (schema == null) {
+        returnTypeIsArray && isNotByteArray || isArray
+    } else {
+        schema.type == "array"
+    }
+
+    /** This constructor overrides the schema directly. The returnType won't be used anymore */
+    constructor(schema: Schema<*>, contentType: String) : this(
+            Object::class.java,
+            schema.type == "array",
+            contentType,
+            schema
+    )
+
+    fun isNonRefType(): Boolean = if (schema == null) {
+        returnType in nonRefTypes
+    } else {
+        true
+    }
+}
+
+/**
+ * Try to determine the content type based on the class
+ */
+fun Class<*>.guessContentType(): String =
+        when (this) {
+            String::class.java -> "text/plain"
+            ByteArray::class.java -> "application/octet-stream"
+            else -> "application/json"
+        }
+
+/**
+ * A set of types that should be inlined, instead of creating a reference, in the OpenApi documentation
+ */
+val nonRefTypes = setOf(
+        String::class.java,
+        Boolean::class.java,
+        Int::class.java,
+        Integer::class.java,
+        List::class.java,
+        Long::class.java,
+        BigDecimal::class.java,
+        Date::class.java,
+        LocalDate::class.java,
+        LocalDateTime::class.java,
+        ByteArray::class.java
+)
+
+fun Content.applyDocumentedContent(documentedContent: DocumentedContent) {
+    when {
+        documentedContent.schema != null -> addMediaType(
+                documentedContent.contentType,
+                MediaType().apply { schema = documentedContent.schema }
+        )
+        documentedContent.isNonRefType() -> when {
+            documentedContent.isArray -> mediaTypeArrayOf(documentedContent.returnType, documentedContent.contentType)
+            else -> mediaType(documentedContent.returnType, documentedContent.contentType)
+        }
+        else -> when {
+            documentedContent.isArray -> mediaTypeArrayOfRef(documentedContent.returnType, documentedContent.contentType)
+            else -> mediaTypeRef(documentedContent.returnType, documentedContent.contentType)
+        }
+    }
+}
+
+fun Components.applyDocumentedContent(documentedResponse: DocumentedContent) {
+    if (!documentedResponse.isNonRefType()) {
+        schema(documentedResponse.returnType)
+    }
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedCrudHandler.kt b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedCrudHandler.kt
new file mode 100644
index 00000000..0bdf7328
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedCrudHandler.kt
@@ -0,0 +1,19 @@
+package io.javalin.plugin.openapi.dsl
+
+import io.javalin.apibuilder.CrudHandler
+import io.javalin.http.Context
+
+class DocumentedCrudHandler(
+        val crudHandlerDocumentation: OpenApiCrudHandlerDocumentation,
+        private val crudHandler: CrudHandler
+) : CrudHandler {
+    override fun getAll(ctx: Context) = crudHandler.getAll(ctx)
+
+    override fun getOne(ctx: Context, resourceId: String) = crudHandler.getOne(ctx, resourceId)
+
+    override fun create(ctx: Context) = crudHandler.create(ctx)
+
+    override fun update(ctx: Context, resourceId: String) = crudHandler.update(ctx, resourceId)
+
+    override fun delete(ctx: Context, resourceId: String) = crudHandler.delete(ctx, resourceId)
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedHandler.kt b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedHandler.kt
new file mode 100644
index 00000000..6a898d6d
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedHandler.kt
@@ -0,0 +1,11 @@
+package io.javalin.plugin.openapi.dsl
+
+import io.javalin.http.Context
+import io.javalin.http.Handler
+
+class DocumentedHandler(
+        val documentation: OpenApiDocumentation,
+        private val handler: Handler
+) : Handler {
+    override fun handle(ctx: Context) = handler.handle(ctx)
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedParameter.kt b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedParameter.kt
new file mode 100644
index 00000000..614d2719
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedParameter.kt
@@ -0,0 +1,16 @@
+package io.javalin.plugin.openapi.dsl
+
+import io.javalin.plugin.openapi.external.findSchema
+import io.swagger.v3.oas.models.parameters.Parameter
+
+class DocumentedParameter(
+        val `in`: String,
+        val name: String,
+        val type: Class<*>
+)
+
+fun Parameter.applyDocumentedParameter(documentedParameter: DocumentedParameter) {
+    `in` = documentedParameter.`in`
+    name = documentedParameter.name
+    schema = findSchema(documentedParameter.type)
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedRequestBody.kt b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedRequestBody.kt
new file mode 100644
index 00000000..61a43b10
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedRequestBody.kt
@@ -0,0 +1,18 @@
+package io.javalin.plugin.openapi.dsl
+
+import io.swagger.v3.oas.models.Components
+import io.swagger.v3.oas.models.parameters.RequestBody
+
+class DocumentedRequestBody(
+        val content: DocumentedContent
+)
+
+fun RequestBody.applyDocumentedRequestBody(documentedRequestBody: DocumentedRequestBody) {
+    updateContent {
+        applyDocumentedContent(documentedRequestBody.content)
+    }
+}
+
+fun Components.applyDocumentedRequestBody(documentedRequestBody: DocumentedRequestBody) {
+    applyDocumentedContent(documentedRequestBody.content)
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedResponse.kt b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedResponse.kt
new file mode 100644
index 00000000..7f502e5f
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedResponse.kt
@@ -0,0 +1,25 @@
+package io.javalin.plugin.openapi.dsl
+
+import io.swagger.v3.oas.models.Components
+import io.swagger.v3.oas.models.responses.ApiResponse
+
+class DocumentedResponse(
+        val status: String,
+        val content: DocumentedContent?
+)
+
+
+fun ApiResponse.applyDocumentedResponse(documentedResponse: DocumentedResponse) {
+    description = ""
+    if (documentedResponse.content != null) {
+        updateContent {
+            applyDocumentedContent(documentedResponse.content)
+        }
+    }
+}
+
+fun Components.applyDocumentedResponse(documentedResponse: DocumentedResponse) {
+    if (documentedResponse.content != null) {
+        applyDocumentedContent(documentedResponse.content)
+    }
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiBuilder.kt b/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiBuilder.kt
new file mode 100644
index 00000000..3ade7b1e
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiBuilder.kt
@@ -0,0 +1,56 @@
+@file:JvmName("OpenApiBuilder")
+
+package io.javalin.plugin.openapi.dsl
+
+import io.javalin.apibuilder.CrudHandler
+import io.javalin.apibuilder.CrudHandlerLambdaKey
+import io.javalin.http.Context
+import io.javalin.http.Handler
+
+/** Creates an instance of the OpenApiDocumentation */
+fun document() = OpenApiDocumentation()
+
+/** Creates an instance of the OpenApiCrudHandlerDocumentation */
+fun documentCrud() = OpenApiCrudHandlerDocumentation()
+
+/** Creates a documented Handler with a lambda */
+fun documented(documentation: OpenApiDocumentation, handle: (ctx: Context) -> Unit) = DocumentedHandler(
+        documentation,
+        Handler { ctx -> handle(ctx) }
+)
+
+/** Creates a documented Handler */
+fun documented(documentation: OpenApiDocumentation, handler: Handler) = DocumentedHandler(
+        documentation,
+        handler
+)
+
+/** Creates a documented CrudHandler */
+fun documented(documentation: OpenApiCrudHandlerDocumentation, handler: CrudHandler) = DocumentedCrudHandler(documentation, handler)
+
+/** Converts a map of handlers and documentations into a map of documented handlers */
+fun <T> documented(documentation: Map<T, OpenApiDocumentation>, handlers: Map<T, Handler>): Map<T, Handler> {
+    if (documentation.keys != handlers.keys) {
+        throw IllegalArgumentException("The keys of the documentation and the handlers don't match. ${documentation.keys} != ${handlers.keys}")
+    }
+    return handlers.mapValues { (key, value) -> documented(documentation.getValue(key), value) }
+}
+
+internal fun documented(crudHandler: CrudHandler, handlers: Map<CrudHandlerLambdaKey, Handler>): Map<CrudHandlerLambdaKey, Handler> {
+    return if (crudHandler is DocumentedCrudHandler) {
+        documented(crudHandler.crudHandlerDocumentation, handlers)
+    } else {
+        handlers
+    }
+}
+
+internal fun documented(crudHandlerDocumentation: OpenApiCrudHandlerDocumentation, handlers: Map<CrudHandlerLambdaKey, Handler>): Map<CrudHandlerLambdaKey, Handler> {
+    val documentations = mapOf(
+            CrudHandlerLambdaKey.GET_ALL to crudHandlerDocumentation.getAllDocumentation,
+            CrudHandlerLambdaKey.GET_ONE to crudHandlerDocumentation.getOneDocumentation,
+            CrudHandlerLambdaKey.CREATE to crudHandlerDocumentation.createDocumentation,
+            CrudHandlerLambdaKey.UPDATE to crudHandlerDocumentation.updateDocumentation,
+            CrudHandlerLambdaKey.DELETE to crudHandlerDocumentation.deleteDocumentation
+    )
+    return documented(documentations, handlers)
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiCrudHandlerDocumentation.kt b/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiCrudHandlerDocumentation.kt
new file mode 100644
index 00000000..dd8a85ef
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiCrudHandlerDocumentation.kt
@@ -0,0 +1,29 @@
+package io.javalin.plugin.openapi.dsl
+
+data class OpenApiCrudHandlerDocumentation(
+        var getAllDocumentation: OpenApiDocumentation = OpenApiDocumentation(),
+        var getOneDocumentation: OpenApiDocumentation = OpenApiDocumentation(),
+        var createDocumentation: OpenApiDocumentation = OpenApiDocumentation(),
+        var updateDocumentation: OpenApiDocumentation = OpenApiDocumentation(),
+        var deleteDocumentation: OpenApiDocumentation = OpenApiDocumentation()
+) {
+    fun getAll(doc: OpenApiDocumentation) = apply {
+        this.getAllDocumentation = doc
+    }
+
+    fun getOne(doc: OpenApiDocumentation) = apply {
+        this.getOneDocumentation = doc
+    }
+
+    fun create(doc: OpenApiDocumentation) = apply {
+        this.createDocumentation = doc
+    }
+
+    fun update(doc: OpenApiDocumentation) = apply {
+        this.updateDocumentation = doc
+    }
+
+    fun delete(doc: OpenApiDocumentation) = apply {
+        this.deleteDocumentation = doc
+    }
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiDocumentation.kt b/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiDocumentation.kt
new file mode 100644
index 00000000..94e62124
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiDocumentation.kt
@@ -0,0 +1,256 @@
+package io.javalin.plugin.openapi.dsl
+
+import io.javalin.plugin.openapi.external.findSchema
+import io.swagger.v3.oas.models.Components
+import io.swagger.v3.oas.models.Operation
+import io.swagger.v3.oas.models.media.ArraySchema
+import io.swagger.v3.oas.models.media.ObjectSchema
+import io.swagger.v3.oas.models.media.Schema
+import io.swagger.v3.oas.models.parameters.Parameter
+import io.swagger.v3.oas.models.parameters.RequestBody
+import io.swagger.v3.oas.models.responses.ApiResponse
+
+class OpenApiDocumentation {
+    val operationUpdaterList = mutableListOf<OpenApiUpdater<Operation>>()
+    val requestBodyList = mutableListOf<OpenApiUpdater<RequestBody>>()
+    val parameterUpdaterListMapping = mutableMapOf<String, MutableList<OpenApiUpdater<Parameter>>>()
+    val responseUpdaterListMapping = mutableMapOf<String, MutableList<OpenApiUpdater<ApiResponse>>>()
+    val componentsUpdaterList = mutableListOf<OpenApiUpdater<Components>>()
+
+    fun hasRequestBodies(): Boolean = requestBodyList.isNotEmpty()
+    fun hasResponses(): Boolean = responseUpdaterListMapping.values.flatten().isNotEmpty()
+
+    // --- OPERATION ---
+    fun operation(applyUpdates: ApplyUpdates<Operation>) = apply {
+        operation(createUpdater(applyUpdates))
+    }
+
+    fun operation(openApiUpdater: OpenApiUpdater<Operation>) = apply {
+        operationUpdaterList.add(openApiUpdater)
+    }
+
+    // --- PATH PARAM ---
+    inline fun <reified T> pathParam(name: String, noinline applyUpdates: ApplyUpdates<Parameter>? = null): OpenApiDocumentation = apply {
+        pathParam(name, T::class.java, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun pathParam(name: String, clazz: Class<*>, openApiUpdater: OpenApiUpdater<Parameter>? = null) = apply {
+        val documentedQueryParameter = DocumentedParameter("path", name, clazz)
+        param(documentedQueryParameter, openApiUpdater)
+    }
+
+    // --- QUERY PARAM ---
+    inline fun <reified T> queryParam(name: String, noinline applyUpdates: ApplyUpdates<Parameter>? = null): OpenApiDocumentation = apply {
+        queryParam(name, T::class.java, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun queryParam(name: String, clazz: Class<*>, openApiUpdater: OpenApiUpdater<Parameter>? = null) = apply {
+        val documentedQueryParameter = DocumentedParameter("query", name, clazz)
+        param(documentedQueryParameter, openApiUpdater)
+    }
+
+    // --- HEADER ---
+    inline fun <reified T> header(name: String, noinline applyUpdates: ApplyUpdates<Parameter>? = null): OpenApiDocumentation = apply {
+        header(name, T::class.java, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun header(name: String, clazz: Class<*>, openApiUpdater: OpenApiUpdater<Parameter>? = null) = apply {
+        val documentedQueryParameter = DocumentedParameter("header", name, clazz)
+        param(documentedQueryParameter, openApiUpdater)
+    }
+
+    // --- COOKIE ---
+    inline fun <reified T> cookie(name: String, noinline applyUpdates: ApplyUpdates<Parameter>? = null): OpenApiDocumentation = apply {
+        cookie(name, T::class.java, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun cookie(name: String, clazz: Class<*>, openApiUpdater: OpenApiUpdater<Parameter>? = null) = apply {
+        val documentedQueryParameter = DocumentedParameter("cookie", name, clazz)
+        param(documentedQueryParameter, openApiUpdater)
+    }
+
+    // --- PARAM ---
+    fun param(documentedParameter: DocumentedParameter, applyUpdates: ApplyUpdates<Parameter>? = null) = apply {
+        param(documentedParameter, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun param(documentedParameter: DocumentedParameter, openApiUpdater: OpenApiUpdater<Parameter>? = null) = apply {
+        val parameterUpdaterList = parameterUpdaterListMapping.getOrSetDefault(documentedParameter.name, mutableListOf())
+        parameterUpdaterList.add { it.applyDocumentedParameter(documentedParameter) }
+        parameterUpdaterList.addIfNotNull(openApiUpdater)
+    }
+
+    // --- UPLOADED FILE ---
+    fun uploadedFile(name: String, applyUpdates: ApplyUpdates<RequestBody>? = null) = apply {
+        uploadedFile(name, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun uploadedFile(name: String, openApiUpdater: OpenApiUpdater<RequestBody>? = null) = apply {
+        val schema = ObjectSchema().apply {
+            properties = mapOf(
+                    name to findSchema(ByteArray::class.java)
+            )
+        }
+        body(schema, "multipart/form-data", openApiUpdater)
+    }
+
+    // --- UPLOADED FILES ---
+    fun uploadedFiles(name: String, applyUpdates: ApplyUpdates<RequestBody>? = null) = apply {
+        uploadedFiles(name, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun uploadedFiles(name: String, openApiUpdater: OpenApiUpdater<RequestBody>? = null) = apply {
+        val schema = ObjectSchema().apply {
+            properties = mapOf(
+                    name to ArraySchema().items(findSchema(ByteArray::class.java))
+            )
+        }
+        body(schema, "multipart/form-data", openApiUpdater)
+    }
+
+    // --- BODY ---
+    inline fun <reified T> body(contentType: String? = null, noinline applyUpdates: ApplyUpdates<RequestBody>? = null) = apply {
+        body(T::class.java, contentType, applyUpdates)
+    }
+
+    fun body(returnType: Class<*>, contentType: String? = null, applyUpdates: ApplyUpdates<RequestBody>? = null) = apply {
+        body(returnType, contentType, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun body(returnType: Class<*>, contentType: String? = null, openApiUpdater: OpenApiUpdater<RequestBody>? = null) = apply {
+        val documentedContent = DocumentedContent(returnType, false, contentType)
+        val documentedBody = DocumentedRequestBody(documentedContent)
+        body(documentedBody, openApiUpdater)
+    }
+
+    @JvmOverloads
+    fun body(schema: Schema<*>, contentType: String, openApiUpdater: OpenApiUpdater<RequestBody>? = null) = apply {
+        val documentedContent = DocumentedContent(schema, contentType)
+        val documentedBody = DocumentedRequestBody(documentedContent)
+        body(documentedBody, openApiUpdater)
+    }
+
+    @JvmOverloads
+    fun body(documentedBody: DocumentedRequestBody, openApiUpdater: OpenApiUpdater<RequestBody>? = null) = apply {
+        componentsUpdaterList.add { it.applyDocumentedRequestBody(documentedBody) }
+        requestBodyList.add { it.applyDocumentedRequestBody(documentedBody) }
+        requestBodyList.addIfNotNull(openApiUpdater)
+    }
+
+    // --- BODY AS BYTES ---
+    @JvmOverloads
+    fun bodyAsBytes(contentType: String? = null, applyUpdates: ApplyUpdates<RequestBody>?) = apply {
+        bodyAsBytes(contentType, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun bodyAsBytes(contentType: String? = null, openApiUpdater: OpenApiUpdater<RequestBody>? = null) = apply {
+        body(ByteArray::class.java, contentType, openApiUpdater)
+    }
+
+    // --- JSON ARRAY ---
+    inline fun <reified T> jsonArray(status: String, noinline applyUpdates: ApplyUpdates<ApiResponse>? = null) = apply {
+        jsonArray(status, T::class.java, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun jsonArray(status: String, returnType: Class<*>, openApiUpdater: OpenApiUpdater<ApiResponse>? = null) = apply {
+        val documentedContent = DocumentedContent(returnType, true, "application/json")
+        val documentedResponse = DocumentedResponse(status, documentedContent)
+        result(documentedResponse, openApiUpdater)
+    }
+
+    // --- JSON ---
+    inline fun <reified T> json(status: String, noinline applyUpdates: ApplyUpdates<ApiResponse>? = null) = apply {
+        json(status, T::class.java, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun json(status: String, returnType: Class<*>, openApiUpdater: OpenApiUpdater<ApiResponse>? = null) = apply {
+        val documentedContent = DocumentedContent(returnType, false, "application/json")
+        val documentedResponse = DocumentedResponse(status, documentedContent)
+        result(documentedResponse, openApiUpdater)
+    }
+
+    // --- HTML ---
+    fun html(status: String, applyUpdates: ApplyUpdates<ApiResponse>? = null) = apply {
+        html(status, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun html(status: String, openApiUpdater: OpenApiUpdater<ApiResponse>? = null) = apply {
+        val documentedContent = DocumentedContent(String::class.java, false, "text/html")
+        val documentedResponse = DocumentedResponse(status, documentedContent)
+        result(documentedResponse, openApiUpdater)
+    }
+
+    // --- RESULT ---
+    inline fun <reified T> result(status: String, noinline applyUpdates: ApplyUpdates<ApiResponse>? = null) = apply {
+        result(status, T::class.java, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun result(status: String, returnType: Class<*>? = null, openApiUpdater: OpenApiUpdater<ApiResponse>? = null) = apply {
+        val documentedContent = if (returnType == null || returnType == Unit::class.java) {
+            null
+        } else {
+            DocumentedContent(returnType, false)
+        }
+        val documentedResponse = DocumentedResponse(status, documentedContent)
+        result(documentedResponse, openApiUpdater)
+    }
+
+    inline fun <reified T> result(status: String, contentType: String?, noinline applyUpdates: ApplyUpdates<ApiResponse>? = null) = apply {
+        result(status, T::class.java, contentType, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun result(status: String, returnType: Class<*>?, contentType: String?, openApiUpdater: OpenApiUpdater<ApiResponse>? = null) = apply {
+        val documentedContent = if (returnType == null || returnType == Unit::class.java) {
+            null
+        } else {
+            DocumentedContent(returnType, false, contentType)
+        }
+        val documentedResponse = DocumentedResponse(status, documentedContent)
+        result(documentedResponse, openApiUpdater)
+    }
+
+    fun result(documentedResponse: DocumentedResponse, applyUpdates: ApplyUpdates<ApiResponse>? = null) = apply {
+        result(documentedResponse, createUpdaterIfNotNull(applyUpdates))
+    }
+
+    @JvmOverloads
+    fun result(documentedResponse: DocumentedResponse, openApiUpdater: OpenApiUpdater<ApiResponse>? = null) = apply {
+        val responseUpdaterList = responseUpdaterListMapping.getOrSetDefault(documentedResponse.status, mutableListOf())
+        componentsUpdaterList.add { it.applyDocumentedResponse(documentedResponse) }
+        responseUpdaterList.add { it.applyDocumentedResponse(documentedResponse) }
+        responseUpdaterList.addIfNotNull(openApiUpdater)
+    }
+}
+
+private fun <T> MutableList<OpenApiUpdater<T>>.add(applyUpdates: ApplyUpdates<T>) {
+    val list = this
+    list.add(createUpdater(applyUpdates))
+}
+
+private fun <T> MutableList<T>.addIfNotNull(item: T?) {
+    val list = this
+    item?.let { list.add(item) }
+}
+
+private fun <K, T> MutableMap<K, T>.getOrSetDefault(key: K, default: T): T {
+    val value = this[key]
+    if (value == null) {
+        this[key] = default
+        return default
+    }
+    return value
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiUpdater.kt b/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiUpdater.kt
new file mode 100644
index 00000000..9bf6e9ce
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/OpenApiUpdater.kt
@@ -0,0 +1,16 @@
+package io.javalin.plugin.openapi.dsl
+
+@FunctionalInterface
+interface OpenApiUpdater<T> {
+    fun applyUpdates(value: T)
+}
+
+typealias ApplyUpdates<T> = (value: T) -> Unit
+
+fun <T> createUpdaterIfNotNull(function: ApplyUpdates<T>?) = function?.let { createUpdater(it) }
+
+fun <T> createUpdater(function: ApplyUpdates<T>) = object : OpenApiUpdater<T> {
+    override fun applyUpdates(value: T) = function(value)
+}
+
+fun <T> List<OpenApiUpdater<T>>.applyAllUpdates(value: T) = this.forEach { it.applyUpdates(value) }
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/extractDocumentation.kt b/src/main/java/io/javalin/plugin/openapi/dsl/extractDocumentation.kt
new file mode 100644
index 00000000..90c26bca
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/extractDocumentation.kt
@@ -0,0 +1,11 @@
+package io.javalin.plugin.openapi.dsl
+
+import io.javalin.core.event.HandlerMetaInfo
+
+fun HandlerMetaInfo.extractDocumentation(): OpenApiDocumentation? {
+    return if (handler is DocumentedHandler) {
+        handler.documentation
+    } else {
+        null
+    }
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/openApiBuilderDSL.kt b/src/main/java/io/javalin/plugin/openapi/dsl/openApiBuilderDSL.kt
new file mode 100644
index 00000000..a0d7f0a0
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/openApiBuilderDSL.kt
@@ -0,0 +1,152 @@
+/**
+ * This file contains helper methods which convert javalin concepts
+ * to OpenApi documentation.
+ */
+package io.javalin.plugin.openapi.dsl
+
+import cc.vileda.openapi.dsl.parameter
+import cc.vileda.openapi.dsl.requestBody
+import cc.vileda.openapi.dsl.response
+import cc.vileda.openapi.dsl.responses
+import io.javalin.core.PathParser
+import io.javalin.core.PathSegment
+import io.javalin.core.event.HandlerMetaInfo
+import io.javalin.http.HandlerType
+import io.javalin.plugin.openapi.external.schema
+import io.swagger.v3.oas.models.Components
+import io.swagger.v3.oas.models.Operation
+import io.swagger.v3.oas.models.PathItem
+import io.swagger.v3.oas.models.Paths
+
+fun Components.applyMetaInfoList(handlerMetaInfoList: List<HandlerMetaInfo>) {
+    handlerMetaInfoList
+            .mapNotNull { it.extractDocumentation() }
+            .flatMap { it.componentsUpdaterList }
+            .applyAllUpdates(this)
+}
+
+fun Paths.applyMetaInfoList(handlerMetaInfoList: List<HandlerMetaInfo>) {
+    handlerMetaInfoList
+            .groupBy { it.path }
+            .forEach { (url, metaInfos) ->
+                val pathParser = PathParser(url)
+                updatePath(pathParser.getOpenApiUrl()) {
+                    applyMetaInfoList(pathParser, metaInfos)
+                }
+            }
+}
+
+fun PathParser.getOpenApiUrl(): String {
+    val segmentsString = segments
+            .joinToString("/") {
+                when (it) {
+                    is PathSegment.Normal -> it.content
+                    /*
+                     * At the moment, OpenApi does not support wildcards. So we just leave it as it is.
+                     * Once it is implemented we can change this.
+                     */
+                    is PathSegment.Wildcard -> "*"
+                    is PathSegment.Parameter -> "{${it.name}}"
+                }
+            }
+    return "/$segmentsString"
+}
+
+fun PathItem.applyMetaInfoList(path: PathParser, handlerMetaInfoList: List<HandlerMetaInfo>) {
+    handlerMetaInfoList
+            .forEach { metaInfo ->
+                val pathItemHttpMethod = metaInfo.httpMethod.asPathItemHttpMethod() ?: return@forEach
+                operation(pathItemHttpMethod, Operation().apply {
+                    applyMetaInfo(path, metaInfo)
+                })
+            }
+}
+
+
+fun Operation.applyMetaInfo(path: PathParser, metaInfo: HandlerMetaInfo) {
+    operationId = metaInfo.createDefaultOperationId(path)
+    summary = metaInfo.createDefaultSummary(path)
+    if (path.pathParamNames.isNotEmpty()) {
+        path.pathParamNames.forEach { pathParamName ->
+            parameter {
+                name = pathParamName
+                `in` = "path"
+                required = true
+                schema(String::class.java)
+            }
+        }
+    }
+
+    metaInfo.extractDocumentation()?.let { documentation ->
+        documentation.parameterUpdaterListMapping
+                .values
+                .forEach { updaters ->
+                    parameter {
+                        updaters.applyAllUpdates(this)
+                    }
+                }
+
+        if (documentation.hasRequestBodies()) {
+            requestBody {
+                documentation.requestBodyList.applyAllUpdates(this)
+            }
+        }
+
+        if (documentation.hasResponses()) {
+            responses {
+                documentation.responseUpdaterListMapping
+                        .forEach { name, updater ->
+                            response(name) {
+                                updater.applyAllUpdates(this)
+                            }
+                        }
+            }
+        }
+        documentation.operationUpdaterList.applyAllUpdates(this)
+    }
+}
+
+fun HandlerType.asPathItemHttpMethod(): PathItem.HttpMethod? = when (this) {
+    HandlerType.GET -> PathItem.HttpMethod.GET
+    HandlerType.PUT -> PathItem.HttpMethod.PUT
+    HandlerType.POST -> PathItem.HttpMethod.POST
+    HandlerType.DELETE -> PathItem.HttpMethod.DELETE
+    HandlerType.OPTIONS -> PathItem.HttpMethod.OPTIONS
+    HandlerType.HEAD -> PathItem.HttpMethod.HEAD
+    HandlerType.PATCH -> PathItem.HttpMethod.PATCH
+    HandlerType.TRACE -> PathItem.HttpMethod.TRACE
+    else -> null
+}
+
+private fun HandlerMetaInfo.createDefaultOperationId(path: PathParser): String {
+    val metaInfo = this
+    val lowerCaseMethod = metaInfo.httpMethod.toString().toLowerCase()
+    val capitalizedPath = path.asReadableWords().joinToString("") { it.capitalize() }
+    return lowerCaseMethod + capitalizedPath
+}
+
+private fun HandlerMetaInfo.createDefaultSummary(path: PathParser): String {
+    val metaInfo = this
+    val capitalizedMethod = metaInfo.httpMethod.toString().toLowerCase().capitalize()
+    return (listOf(capitalizedMethod) + path.asReadableWords()).joinToString(" ") { it.trim() }
+}
+
+private fun PathParser.asReadableWords(): List<String> {
+    val words = mutableListOf<String>()
+    segments.forEach { segment ->
+        when (segment) {
+            is PathSegment.Normal -> words.add(segment.content)
+            is PathSegment.Wildcard -> words.addAll(arrayOf("with", "wildcard"))
+            is PathSegment.Parameter -> {
+                words.add("with")
+                words.add(segment.name
+                        .split("-")
+                        .map { it.toLowerCase() }
+                        .mapIndexed { index, s -> if (index > 0) s.capitalize() else s }
+                        .joinToString("")
+                )
+            }
+        }
+    }
+    return words
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/dsl/openApiUpdaterDSL.kt b/src/main/java/io/javalin/plugin/openapi/dsl/openApiUpdaterDSL.kt
new file mode 100644
index 00000000..4b30807d
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/dsl/openApiUpdaterDSL.kt
@@ -0,0 +1,39 @@
+/**
+ * This file contains helper methods to update openapi items without overriding the original
+ */
+package io.javalin.plugin.openapi.dsl
+
+import io.swagger.v3.oas.models.Components
+import io.swagger.v3.oas.models.OpenAPI
+import io.swagger.v3.oas.models.PathItem
+import io.swagger.v3.oas.models.Paths
+import io.swagger.v3.oas.models.media.Content
+import io.swagger.v3.oas.models.parameters.RequestBody
+import io.swagger.v3.oas.models.responses.ApiResponse
+
+internal fun OpenAPI.updateComponents(apply: Components.() -> Unit) {
+    components = components ?: Components()
+    components.apply(apply)
+}
+
+internal fun OpenAPI.updatePaths(apply: Paths.() -> Unit) {
+    paths = paths ?: Paths()
+    paths.apply(apply)
+}
+
+internal fun Paths.updatePath(name: String, apply: PathItem.() -> Unit) {
+    if (get(name) == null) {
+        addPathItem(name, PathItem())
+    }
+    get(name)!!.apply(apply)
+}
+
+internal fun RequestBody.updateContent(apply: Content.() -> Unit) {
+    content = content ?: Content()
+    content.apply(apply)
+}
+
+internal fun ApiResponse.updateContent(apply: Content.() -> Unit) {
+    content = content ?: Content()
+    content.apply(apply)
+}
diff --git a/src/main/java/io/javalin/plugin/openapi/external/KotlinOpenApiDsl.kt b/src/main/java/io/javalin/plugin/openapi/external/KotlinOpenApiDsl.kt
new file mode 100644
index 00000000..bfa8f68c
--- /dev/null
+++ b/src/main/java/io/javalin/plugin/openapi/external/KotlinOpenApiDsl.kt
@@ -0,0 +1,140 @@
+package io.javalin.plugin.openapi.external
+
+import io.swagger.v3.core.converter.ModelConverters
+import io.swagger.v3.oas.models.Components
+import io.swagger.v3.oas.models.examples.Example
+import io.swagger.v3.oas.models.media.ArraySchema
+import io.swagger.v3.oas.models.media.BooleanSchema
+import io.swagger.v3.oas.models.media.Content
+import io.swagger.v3.oas.models.media.DateSchema
+import io.swagger.v3.oas.models.media.DateTimeSchema
+import io.swagger.v3.oas.models.media.IntegerSchema
+import io.swagger.v3.oas.models.media.MediaType
+import io.swagger.v3.oas.models.media.Schema
+import io.swagger.v3.oas.models.media.StringSchema
+import io.swagger.v3.oas.models.parameters.Parameter
+import java.math.BigDecimal
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.util.*
+
+// The original functions are in https://github.com/derveloper/kotlin-openapi3-dsl/blob/master/src/main/kotlin/cc/vileda/openapi/dsl/OpenApiDsl.kt
+// I needed to create a copy of all the inline functions, as it is not possible to call kotlin inline function from java code
+
+internal fun <T> Components.schema(clazz: Class<T>, init: Schema<*>.() -> Unit) {
+    if (schemas == null) {
+        schemas = mutableMapOf()
+    }
+    val schema = findSchema(clazz)
+    schema!!.init()
+    schemas.put(clazz.simpleName, schema)
+}
+
+internal fun <T> Components.schema(clazz: Class<T>) {
+    if (schemas == null) {
+        schemas = mutableMapOf()
+    }
+    val schema = findSchema(clazz)
+    schemas.put(clazz.simpleName, schema)
+}
+
+internal fun <T> Parameter.schema(clazz: Class<T>, init: Schema<*>.() -> Unit) {
+    schema = findSchema(clazz)
+    schema.init()
+}
+
+internal fun <T> Parameter.schema(clazz: Class<T>) {
+    schema = findSchema(clazz)
+}
+
+internal fun <T> mediaType(clazz: Class<T>): MediaType {
+    val mediaType = MediaType()
+    val modelSchema = findSchema(clazz)
+    mediaType.schema = modelSchema
+    return mediaType
+}
+
+/**
+ * This helper is not in the original library.
+ * Because it is not possible to create a list instance of an unknown type we need a separate method for that.
+ */
+internal fun mediaTypeOfList(): MediaType {
+    val mediaType = MediaType()
+    val modelSchema = ArraySchema()
+    mediaType.schema = modelSchema
+    return mediaType
+}
+
+internal fun <T> mediaTypeRef(clazz: Class<T>): MediaType {
+    val mediaType = MediaType()
+    mediaType.schema = Schema<T>()
+    mediaType.schema.`$ref` = clazz.simpleName
+    return mediaType
+}
+
+internal fun <T> Content.mediaTypeRef(clazz: Class<T>, name: String) {
+    val mediaType = mediaTypeRef(clazz)
+    addMediaType(name, mediaType)
+}
+
+internal fun <T> Content.mediaType(clazz: Class<T>, name: String) {
+    val mediaType = mediaType(clazz)
+    addMediaType(name, mediaType)
+}
+
+internal fun <T> Content.mediaTypeArrayOfRef(clazz: Class<T>, name: String) {
+    val mediaTypeArray = mediaTypeOfList()
+    val mediaTypeObj = mediaTypeRef(clazz)
+    (mediaTypeArray.schema as ArraySchema).items(mediaTypeObj.schema)
+    addMediaType(name, mediaTypeArray)
+}
+
+internal fun <T> Content.mediaTypeArrayOf(clazz: Class<T>, name: String) {
+    val mediaTypeArray = mediaTypeOfList()
+    val mediaTypeObj = mediaType(clazz)
+    (mediaTypeArray.schema as ArraySchema).items(mediaTypeObj.schema)
+    addMediaType(name, mediaTypeArray)
+}
+
+internal fun <T> findSchema(clazz: Class<T>): Schema<*>? {
+    return getEnumSchema(clazz) ?: when (clazz) {
+        String::class.java -> StringSchema()
+        Boolean::class.java -> BooleanSchema()
+        java.lang.Boolean::class.java -> BooleanSchema()
+        Int::class.java -> IntegerSchema()
+        Integer::class.java -> IntegerSchema()
+        List::class.java -> ArraySchema()
+        Long::class.java -> IntegerSchema().format("int64")
+        BigDecimal::class.java -> IntegerSchema().format("")
+        Date::class.java -> DateSchema()
+        LocalDate::class.java -> DateSchema()
+        LocalDateTime::class.java -> DateTimeSchema()
+        /* BEGIN Custom classes */
+        ByteArray::class.java -> StringSchema().format("binary")
+        /* END Custom classes */
+        else -> ModelConverters.getInstance().read(clazz)[clazz.simpleName]
+    }
+}
+
+internal fun <T> getEnumSchema(clazz: Class<T>): Schema<*>? {
+    val values = clazz.enumConstants ?: return null
+
+    val schema = StringSchema()
+    for (enumVal in values) {
+        schema.addEnumItem(enumVal.toString())
+    }
+    return schema
+}
+
+
+internal fun <T> MediaType.example(clazz: Class<T>, value: T, init: Example.() -> Unit) {
+    if (examples == null) {
+        examples = mutableMapOf()
+    }
+
+    val example = Example()
+    example.value = value
+    example.init()
+    examples[clazz.simpleName] = example
+}
+


Test patch:
diff --git a/src/test/java/io/javalin/openapi/TestOpenApi.kt b/src/test/java/io/javalin/openapi/TestOpenApi.kt
new file mode 100644
index 00000000..4cdb7828
--- /dev/null
+++ b/src/test/java/io/javalin/openapi/TestOpenApi.kt
@@ -0,0 +1,297 @@
+/*
+ * Javalin - https://javalin.io
+ * Copyright 2017 David Åse
+ * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+ */
+package io.javalin.openapi
+
+import cc.vileda.openapi.dsl.components
+import cc.vileda.openapi.dsl.externalDocs
+import cc.vileda.openapi.dsl.get
+import cc.vileda.openapi.dsl.info
+import cc.vileda.openapi.dsl.openapiDsl
+import cc.vileda.openapi.dsl.path
+import cc.vileda.openapi.dsl.paths
+import cc.vileda.openapi.dsl.security
+import cc.vileda.openapi.dsl.securityScheme
+import cc.vileda.openapi.dsl.server
+import cc.vileda.openapi.dsl.tag
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.databind.SerializationFeature
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import io.javalin.Javalin
+import io.javalin.TestUtil
+import io.javalin.apibuilder.ApiBuilder.crud
+import io.javalin.apibuilder.CrudHandler
+import io.javalin.http.Context
+import io.javalin.plugin.json.JavalinJackson
+import io.javalin.plugin.openapi.JavalinOpenApi
+import io.javalin.plugin.openapi.OpenApiOptions
+import io.javalin.plugin.openapi.dsl.OpenApiDocumentation
+import io.javalin.plugin.openapi.dsl.document
+import io.javalin.plugin.openapi.dsl.documentCrud
+import io.javalin.plugin.openapi.dsl.documented
+import io.swagger.v3.oas.models.OpenAPI
+import io.swagger.v3.oas.models.info.Info
+import io.swagger.v3.oas.models.security.SecurityScheme
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.assertThatExceptionOfType
+import org.junit.Before
+import org.junit.Test
+
+class User(val name: String)
+
+fun createComplexExampleBaseConfiguration() = openapiDsl {
+    info {
+        title = "Example"
+        version = "1.0.0"
+    }
+    server {
+        url = "https://app.example"
+        description = "My example app"
+    }
+    paths {
+        path("/unimplemented") {
+            get {
+                summary = "This path is not implemented in javalin"
+            }
+        }
+        path("/user") {
+            description = "Some additional information for the /user endpoint"
+        }
+    }
+    components {
+        securityScheme {
+            type = SecurityScheme.Type.HTTP
+            scheme = "basic"
+        }
+    }
+    security {
+        addList("http")
+    }
+    tag {
+        name = "user"
+        description = "User operations"
+    }
+    externalDocs {
+        description = "Find more info here"
+        url = "https://external-documentation.info"
+    }
+}
+
+class TestOpenApi {
+    @Before
+    fun resetObjectMapper() {
+        JavalinJackson.configure(
+                jacksonObjectMapper()
+                        .enable(SerializationFeature.INDENT_OUTPUT)
+                        .setSerializationInclusion(JsonInclude.Include.NON_NULL)
+        )
+    }
+
+    @Test
+    fun `createSchema() work with complexExample and dsl`() {
+        val app = Javalin.create {
+            it.enableOpenApi(OpenApiOptions(::createComplexExampleBaseConfiguration))
+        }
+
+        val getUserDocumentation = document()
+                .operation {
+                    it.description = "Get a specific user"
+                    it.summary = "Get current user"
+                    it.operationId = "getCurrentUser"
+                    it.deprecated = true
+                    it.addTagsItem("user")
+                }
+                .json<User>("200") {
+                    it.description = "Request successful"
+                }
+        app.get("/user", documented(getUserDocumentation) {
+            it.json(User(name = "Jim"))
+        })
+
+        val getUsersDocumentation = document()
+                .operation {
+                    it.addTagsItem("user")
+                }
+                .cookie<String>("my-cookie") {
+                    it.description = "My cookie"
+                }
+                .header<String>("x-my-header") {
+                    it.description = "My header"
+                }
+                .pathParam<String>("my-path-param") {
+                    it.description = "My path param"
+                }
+                .queryParam<String>("name") {
+                    it.description = "The name of the users you want to filter"
+                    it.required = true
+                    it.deprecated = true
+                    it.allowEmptyValue = true
+                }
+                .queryParam<Int>("age")
+                .jsonArray<User>("200")
+        app.get("/users", documented(getUsersDocumentation) {
+            val myCookie = it.cookie("my-cookie")
+            val myHeader = it.cookie("x-my-header")
+            val nameFilter = it.queryParam("name")
+            val ageFilter = it.queryParam("age")
+            it.json(listOf(User(name = "Jim")))
+        })
+
+        val getUsers2Documentation = document()
+                .operation {
+                    it.addTagsItem("user")
+                }
+                .json<Array<User>>("200")
+        app.get("/users2", documented(getUsers2Documentation) { it.json(listOf(User(name = "Jim"))) })
+
+        val putUserDocumentation = document()
+                .operation {
+                    it.addTagsItem("user")
+                }
+                .body<String> {
+                    it.description = "body description"
+                    it.required = true
+                }
+                .body<User>()
+                .bodyAsBytes()
+                .bodyAsBytes("image/png")
+        app.put("/user", documented(putUserDocumentation) {
+            val userString = it.body()
+            val user = it.bodyAsClass(User::class.java)
+            val userAsBytes = it.bodyAsBytes()
+            val userImage = it.bodyAsBytes()
+        })
+
+        val getStringDocumentation = OpenApiDocumentation()
+                .result<String>("200")
+        app.get("/string", documented(getStringDocumentation) {
+            it.result("Hello")
+        })
+
+        val getHomepageDocumentation = OpenApiDocumentation()
+                .html("200") {
+                    it.description = "My Homepage"
+                }
+        app.get("/homepage", documented(getHomepageDocumentation) {
+            it.html("<p>Hello World</p>")
+        })
+
+        val getUploadDocumentation = OpenApiDocumentation()
+                .uploadedFile("file") {
+                    it.description = "MyFile"
+                    it.required = true
+                }
+        app.get("/upload", documented(getUploadDocumentation) {
+            it.uploadedFile("file")
+        })
+
+        val getUploadsDocumentation = OpenApiDocumentation()
+                .uploadedFiles("files") {
+                    it.description = "MyFiles"
+                    it.required = true
+                }
+        app.get("/uploads", documented(getUploadsDocumentation) {
+            it.uploadedFiles("files")
+        })
+
+        val getResourcesDocumentation = OpenApiDocumentation()
+                .result<Unit>("200")
+        app.get("/resources/*", documented(getResourcesDocumentation) {})
+
+        val actual = JavalinOpenApi.createSchema(app)
+
+        assertThat(actual.asJsonString()).isEqualTo(complexExampleJson)
+    }
+
+    @Test
+    fun `createSchema() work with crudHandler and dsl`() {
+        val app = Javalin.create {
+            it.enableOpenApi(OpenApiOptions { OpenAPI().info(Info().title("Example").version("1.0.0")) })
+        }
+
+        val userCrudHandlerDocumentation = documentCrud()
+                .getOne(document().json<User>("200"))
+                .getAll(document().jsonArray<User>("200"))
+
+
+        class UserCrudHandler : CrudHandler {
+            override fun getAll(ctx: Context) {
+            }
+
+            override fun getOne(ctx: Context, resourceId: String) {
+            }
+
+            override fun create(ctx: Context) {
+            }
+
+            override fun update(ctx: Context, resourceId: String) {
+            }
+
+            override fun delete(ctx: Context, resourceId: String) {
+            }
+        }
+
+        app.routes {
+            crud("users/:user-id", documented(userCrudHandlerDocumentation, UserCrudHandler()))
+        }
+
+        val actual = JavalinOpenApi.createSchema(app)
+
+        assertThat(actual.asJsonString()).isEqualTo(crudExampleJson)
+    }
+
+    @Test
+    fun `createSchema() throw error if enableOpenApi is not activated`() {
+        val app = Javalin.create()
+
+        val getUserDocumentation = document().result<User>("200")
+        app.get("/user", documented(getUserDocumentation) { it.json(User(name = "Jim")) })
+
+        app.put("/user") {}
+
+        assertThatExceptionOfType(IllegalStateException::class.java)
+                .isThrownBy {
+                    JavalinOpenApi.createSchema(app)
+                }
+                .withMessage("You need to activate the \"enableOpenApi\" option before you can create the OpenAPI schema")
+    }
+
+    @Test
+    fun `enableOpenApi() provide get route if path is given`() {
+        TestUtil.test(Javalin.create {
+            it.enableOpenApi(OpenApiOptions(
+                    "/docs/swagger.json",
+                    Info().apply {
+                        title = "Example"
+                        version = "1.0.0"
+                    }
+            ))
+        }) { app, http ->
+            app.get("/test") {}
+
+            val actual = http.jsonGet("/docs/swagger.json").body
+
+            assertThat(actual).isEqualTo(provideRouteExampleJson)
+        }
+    }
+
+    @Test
+    fun `enableOpenApi() provide get route if path is given with baseConfiguration`() {
+        TestUtil.test(Javalin.create {
+            it.enableOpenApi(OpenApiOptions("/docs/swagger.json") {
+                OpenAPI().info(Info().apply {
+                    title = "Example"
+                    version = "1.0.0"
+                })
+            })
+        }) { app, http ->
+            app.get("/test") {}
+
+            val actual = http.jsonGet("/docs/swagger.json").body
+
+            assertThat(actual).isEqualTo(provideRouteExampleJson)
+        }
+    }
+}
diff --git a/src/test/java/io/javalin/openapi/json.kt b/src/test/java/io/javalin/openapi/json.kt
new file mode 100644
index 00000000..5aa457a0
--- /dev/null
+++ b/src/test/java/io/javalin/openapi/json.kt
@@ -0,0 +1,442 @@
+/**
+ * This file contains the json definitions required for the test
+ */
+package io.javalin.openapi
+
+import com.fasterxml.jackson.databind.JsonNode
+import io.javalin.plugin.json.JavalinJackson
+import io.swagger.v3.oas.models.OpenAPI
+import org.intellij.lang.annotations.Language
+
+fun String.formatJson(): String {
+    val node = JavalinJackson.fromJson(this, JsonNode::class.java)
+    return JavalinJackson.toJson(node)
+}
+
+fun OpenAPI.asJsonString(): String = JavalinJackson.toJson(this)
+
+// This variable is needed for the jsons. That's how I can avoid the ugly """${"$"}""".
+val ref = "\$ref"
+
+@Language("JSON")
+val userOpenApiSchema = """
+{
+   "required":[
+      "name"
+   ],
+   "type":"object",
+   "properties":{
+      "name":{
+         "type":"string"
+      }
+   }
+}
+""".formatJson()
+
+@Language("JSON")
+val complexExampleUsersGetResponsesJson = """
+{
+  "200": {
+    "description": "",
+    "content": {
+      "application/json": {
+        "schema": {
+          "type": "array",
+          "items": {
+            "$ref": "#/components/schemas/User"
+          }
+        }
+      }
+    }
+  }
+}
+""".formatJson()
+
+@Language("JSON")
+val provideRouteExampleJson = """
+  {
+    "openapi": "3.0.1",
+    "info": {
+      "title": "Example",
+      "version": "1.0.0"
+    },
+    "paths": {
+      "/docs/swagger.json": {
+        "get": {
+          "summary": "Get docs swagger.json",
+          "operationId": "getDocsSwagger.json"
+        }
+      },
+      "/test": {
+        "get": {
+          "summary": "Get test",
+          "operationId": "getTest"
+        }
+      }
+    },
+    "components": {}
+  }
+""".formatJson()
+
+@Language("JSON")
+val complexExampleJson = """
+{
+  "openapi": "3.0.1",
+  "info": {
+    "title": "Example",
+    "version": "1.0.0"
+  },
+  "externalDocs": {
+    "description": "Find more info here",
+    "url": "https://external-documentation.info"
+  },
+  "servers": [
+    {
+      "url": "https://app.example",
+      "description": "My example app"
+    }
+  ],
+  "security": [
+    {
+      "http": []
+    }
+  ],
+  "tags": [
+    {
+      "name": "user",
+      "description": "User operations"
+    }
+  ],
+  "paths": {
+    "/unimplemented": {
+      "get": {
+        "summary": "This path is not implemented in javalin"
+      }
+    },
+    "/user": {
+      "description": "Some additional information for the /user endpoint",
+      "get": {
+        "tags": ["user"],
+        "summary": "Get current user",
+        "description": "Get a specific user",
+        "operationId": "getCurrentUser",
+        "responses": {
+          "200": {
+            "description": "Request successful",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/User"
+                }
+              }
+            }
+          }
+        },
+        "deprecated": true
+      },
+      "put": {
+        "tags": ["user"],
+        "summary": "Put user",
+        "operationId": "putUser",
+        "requestBody": {
+          "description": "body description",
+          "content": {
+            "text/plain": {
+              "schema": {
+                "type": "string"
+              }
+            },
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/User"
+              }
+            },
+            "application/octet-stream": {
+              "schema": {
+                "type": "string",
+                "format": "binary"
+              }
+            },
+            "image/png": {
+              "schema": {
+                "type": "string",
+                "format": "binary"
+              }
+            }
+          },
+          "required": true
+        }
+      }
+    },
+    "/users": {
+      "get": {
+        "tags": ["user"],
+        "summary": "Get users",
+        "operationId": "getUsers",
+        "parameters": [
+          {
+            "name": "my-cookie",
+            "in": "cookie",
+            "description": "My cookie",
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "x-my-header",
+            "in": "header",
+            "description": "My header",
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "my-path-param",
+            "in": "path",
+            "description": "My path param",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "name",
+            "in": "query",
+            "description": "The name of the users you want to filter",
+            "required": true,
+            "deprecated": true,
+            "allowEmptyValue": true,
+            "schema": {
+              "type": "string"
+            }
+          },
+          {
+            "name": "age",
+            "in": "query",
+            "schema": {
+              "type": "integer",
+              "format": "int32"
+            }
+          }
+        ],
+        "responses": $complexExampleUsersGetResponsesJson
+      }
+    },
+    "/users2": {
+      "get": {
+        "tags": ["user"],
+        "summary": "Get users2",
+        "operationId": "getUsers2",
+        "responses": $complexExampleUsersGetResponsesJson
+      }
+    },
+    "/string": {
+      "get": {
+        "summary": "Get string",
+        "operationId": "getString",
+        "responses": {
+          "200": {
+            "description": "",
+            "content": {
+              "text/plain": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/homepage": {
+      "get": {
+        "summary": "Get homepage",
+        "operationId": "getHomepage",
+        "responses": {
+          "200": {
+            "description": "My Homepage",
+            "content": {
+              "text/html": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/upload": {
+      "get": {
+        "summary": "Get upload",
+        "operationId": "getUpload",
+        "requestBody": {
+          "description": "MyFile",
+          "content": {
+            "multipart/form-data": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "file": {
+                    "type": "string",
+                    "format": "binary"
+                  }
+                }
+              }
+            }
+          },
+          "required": true
+        }
+      }
+    },
+    "/uploads": {
+      "get": {
+        "summary": "Get uploads",
+        "operationId": "getUploads",
+        "requestBody": {
+          "description": "MyFiles",
+          "content": {
+            "multipart/form-data": {
+              "schema": {
+                "type": "object",
+                "properties": {
+                  "files": {
+                    "type": "array",
+                    "items": {
+                      "type": "string",
+                      "format": "binary"
+                    }
+                  }
+                }
+              }
+            }
+          },
+          "required": true
+        }
+      }
+    },
+    "/resources/*": {
+      "get": {
+        "summary": "Get resources with wildcard",
+        "operationId": "getResourcesWithWildcard",
+        "responses": {
+          "200": {
+            "description": ""
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "User": $userOpenApiSchema
+    },
+    "securitySchemes": {
+      "http": {
+        "type": "HTTP",
+        "scheme": "basic"
+      }
+    }
+  }
+}
+""".formatJson()
+
+
+@Language("JSON")
+val crudExampleJson = """
+{
+  "openapi": "3.0.1",
+  "info": {
+    "title": "Example",
+    "version": "1.0.0"
+  },
+  "paths": {
+    "/users/{user-id}": {
+      "get": {
+        "summary": "Get users with userId",
+        "operationId": "getUsersWithUserId",
+        "parameters": [
+          {
+            "name": "user-id",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/User"
+                }
+              }
+            }
+          }
+        }
+      },
+      "delete": {
+        "summary": "Delete users with userId",
+        "operationId": "deleteUsersWithUserId",
+        "parameters": [
+          {
+            "name": "user-id",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ]
+      },
+      "patch": {
+        "summary": "Patch users with userId",
+        "operationId": "patchUsersWithUserId",
+        "parameters": [
+          {
+            "name": "user-id",
+            "in": "path",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ]
+      }
+    },
+    "/users": {
+      "get": {
+        "summary": "Get users",
+        "operationId": "getUsers",
+        "responses": {
+          "200": {
+            "description": "",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/User"
+                  }
+                }
+              }
+            }
+          }
+        }
+      },
+      "post": {
+        "summary": "Post users",
+        "operationId": "postUsers"
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "User": $userOpenApiSchema
+    }
+  }
+}
+""".formatJson()