Repository: bkbnio/kompendium (kotlin)

Issue:
Top level unstructured data is not supported 
**Describe the bug**
When trying to document an Endpoint that responds a plain `JsonElement`, I'm no longer to start my application. It crashes with this exception:
```
[main] INFO ktor.application - Autoreload is disabled because the development mode is off.
Exception in thread "main" java.lang.IllegalStateException: Unable to parse field type from val kotlinx.serialization.json.JsonPrimitive.content: kotlin.String
	at io.bkbn.kompendium.core.handler.ObjectHandler.handleDefault(ObjectHandler.kt:88)
	at io.bkbn.kompendium.core.handler.ObjectHandler.generateFieldMap(ObjectHandler.kt:70)
	at io.bkbn.kompendium.core.handler.ObjectHandler.handle(ObjectHandler.kt:50)
	at io.bkbn.kompendium.core.Kontent$generateKTypeKontent$2.invoke(Kontent.kt:96)
	at io.bkbn.kompendium.core.util.Helpers.logged(Helpers.kt:26)
	at io.bkbn.kompendium.core.Kontent.generateKTypeKontent(Kontent.kt:78)
	at io.bkbn.kompendium.core.Kontent.generateKontent(Kontent.kt:55)
	at io.bkbn.kompendium.core.KompendiumPreFlight.addToCache(KompendiumPreFlight.kt:38)
	at io.bkbn.kompendium.playground.BasicPlaygroundKt$mainModule$3$5.invoke(BasicPlayground.kt:225)
	at io.bkbn.kompendium.playground.BasicPlaygroundKt$mainModule$3$5.invoke(BasicPlayground.kt:106)
	at io.ktor.routing.RoutingBuilderKt.route(RoutingBuilder.kt:18)
	at io.bkbn.kompendium.playground.BasicPlaygroundKt$mainModule$3.invoke(BasicPlayground.kt:106)
	at io.bkbn.kompendium.playground.BasicPlaygroundKt$mainModule$3.invoke(BasicPlayground.kt:76)
	at io.ktor.routing.RoutingKt.routing(Routing.kt:129)
	at io.bkbn.kompendium.playground.BasicPlaygroundKt.mainModule(BasicPlayground.kt:76)
	at io.bkbn.kompendium.playground.BasicPlaygroundKt.access$mainModule(BasicPlayground.kt:1)
	at io.bkbn.kompendium.playground.BasicPlaygroundKt$main$1.invoke(BasicPlayground.kt:57)
	at io.bkbn.kompendium.playground.BasicPlaygroundKt$main$1.invoke(BasicPlayground.kt:57)
	at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$instantiateAndConfigureApplication$1.invoke(ApplicationEngineEnvironmentReloading.kt:321)
	at io.ktor.server.engine.ApplicationEngineEnvironmentReloading$instantiateAndConfigureApplication$1.invoke(ApplicationEngineEnvironmentReloading.kt:310)
	at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.avoidingDoubleStartup(ApplicationEngineEnvironmentReloading.kt:338)
	at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.instantiateAndConfigureApplication(ApplicationEngineEnvironmentReloading.kt:310)
	at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.createApplication(ApplicationEngineEnvironmentReloading.kt:143)
	at io.ktor.server.engine.ApplicationEngineEnvironmentReloading.start(ApplicationEngineEnvironmentReloading.kt:277)
	at io.ktor.server.netty.NettyApplicationEngine.start(NettyApplicationEngine.kt:174)
	at io.bkbn.kompendium.playground.BasicPlaygroundKt.main(BasicPlayground.kt:58)
	at io.bkbn.kompendium.playground.BasicPlaygroundKt.main(BasicPlayground.kt)
```

**To Reproduce**
See: https://github.com/MarcelBochtler/kompendium/commit/045e40af73425d599ec37b4d50a20f6cb1ab8c67

**Expected behavior**
The application is able to start and displays the serialized JsonElement in the OpenApi docs.

Pull request description:
# Description

Allows unstructured data to be declared at the top level

Closes #217 

## Type of change

Please delete options that are not relevant.

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation 
- [ ] Chore

# How Has This Been Tested?

Added test

# Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] I have updated the CHANGELOG in the `Unreleased` section
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a344d67f7e..6fb42d109c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,10 @@
 
 ## Released
 
+## [2.3.1] - March 5th, 2022
+### Changed
+- Can now apply `@FreeFormObject` to top level types
+
 ## [2.3.0] - March 1st, 2022
 ### Added
 - Brand new SwaggerUI support as a KTor plugin with WebJar under the hood and flexible configuration
diff --git a/gradle.properties b/gradle.properties
index e4cacedf84..a4b23948bb 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,5 +1,5 @@
 # Kompendium
-project.version=2.3.0
+project.version=2.3.1
 # Kotlin
 kotlin.code.style=official
 # Gradle
diff --git a/kompendium-annotations/src/main/kotlin/io/bkbn/kompendium/annotations/FreeFormObject.kt b/kompendium-annotations/src/main/kotlin/io/bkbn/kompendium/annotations/FreeFormObject.kt
index b7de9ed8ec..d64b903c07 100644
--- a/kompendium-annotations/src/main/kotlin/io/bkbn/kompendium/annotations/FreeFormObject.kt
+++ b/kompendium-annotations/src/main/kotlin/io/bkbn/kompendium/annotations/FreeFormObject.kt
@@ -1,5 +1,5 @@
 package io.bkbn.kompendium.annotations
 
 @Retention(AnnotationRetention.RUNTIME)
-@Target(AnnotationTarget.PROPERTY)
+@Target(AnnotationTarget.PROPERTY, AnnotationTarget.CLASS)
 annotation class FreeFormObject
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/handler/ObjectHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/handler/ObjectHandler.kt
index 39fabea7d9..112b9e50ed 100644
--- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/handler/ObjectHandler.kt
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/handler/ObjectHandler.kt
@@ -24,6 +24,7 @@ import kotlin.reflect.KProperty1
 import kotlin.reflect.KType
 import kotlin.reflect.full.createType
 import kotlin.reflect.full.findAnnotation
+import kotlin.reflect.full.hasAnnotation
 import kotlin.reflect.full.memberProperties
 import kotlin.reflect.jvm.javaField
 import org.slf4j.LoggerFactory
@@ -44,19 +45,24 @@ object ObjectHandler : SchemaHandler {
     // Only analyze if component has not already been stored in the cache
     if (!cache.containsKey(slug)) {
       logger.debug("$slug was not found in cache, generating now")
-      // todo this should be some kind of empty schema at this point, then throw error if not updated eventually
-      cache[type.getSimpleSlug()] = ReferencedSchema(type.getReferenceSlug())
-      val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap()
-      val fieldMap = clazz.generateFieldMap(typeMap, cache)
-        .plus(clazz.generateUndeclaredFieldMap(cache))
-        .mapValues { (_, fieldSchema) ->
-          val fieldSlug = cache.filter { (_, vv) -> vv == fieldSchema }.keys.firstOrNull()
-          postProcessSchema(fieldSchema, fieldSlug)
-        }
-      logger.debug("$slug contains $fieldMap")
-      val schema = ObjectSchema(fieldMap).adjustForRequiredParams(clazz)
-      logger.debug("$slug schema: $schema")
-      cache[slug] = schema
+      // check if free form object
+      if (clazz.hasAnnotation<FreeFormObject>()) {
+        cache[type.getSimpleSlug()] = FreeFormSchema()
+      } else {
+        // todo this should be some kind of empty schema at this point, then throw error if not updated eventually
+        cache[type.getSimpleSlug()] = ReferencedSchema(type.getReferenceSlug())
+        val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap()
+        val fieldMap = clazz.generateFieldMap(typeMap, cache)
+          .plus(clazz.generateUndeclaredFieldMap(cache))
+          .mapValues { (_, fieldSchema) ->
+            val fieldSlug = cache.filter { (_, vv) -> vv == fieldSchema }.keys.firstOrNull()
+            postProcessSchema(fieldSchema, fieldSlug)
+          }
+        logger.debug("$slug contains $fieldMap")
+        val schema = ObjectSchema(fieldMap).adjustForRequiredParams(clazz)
+        logger.debug("$slug schema: $schema")
+        cache[slug] = schema
+      }
     }
   }
 


Test patch:
diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt
index 0d3ebeded5..e827ee7ec9 100644
--- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt
+++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt
@@ -18,6 +18,7 @@ import io.bkbn.kompendium.core.util.exampleParams
 import io.bkbn.kompendium.core.util.exclusiveMinMax
 import io.bkbn.kompendium.core.util.formattedParam
 import io.bkbn.kompendium.core.util.formattedType
+import io.bkbn.kompendium.core.util.freeFormField
 import io.bkbn.kompendium.core.util.freeFormObject
 import io.bkbn.kompendium.core.util.genericPolymorphicResponse
 import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls
@@ -302,6 +303,9 @@ class KompendiumTest : DescribeSpec({
   }
   describe("Free Form") {
     it("Can create a free-form field") {
+      openApiTestAllSerializers("free_form_field.json") { freeFormField() }
+    }
+    it("Can create a top-level free form object") {
       openApiTestAllSerializers("free_form_object.json") { freeFormObject() }
     }
   }
diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt
index 4071433e84..f77d4d6c4c 100644
--- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt
+++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt
@@ -579,6 +579,16 @@ fun Application.multipleOfDouble() {
   }
 }
 
+fun Application.freeFormField() {
+  routing {
+    route("/test/required_param") {
+      notarizedGet(TestResponseInfo.freeFormField) {
+        call.respond(HttpStatusCode.OK, TestResponse("hi"))
+      }
+    }
+  }
+}
+
 fun Application.freeFormObject() {
   routing {
     route("/test/required_param") {
diff --git a/kompendium-core/src/test/resources/free_form_field.json b/kompendium-core/src/test/resources/free_form_field.json
new file mode 100644
index 0000000000..8eae097a34
--- /dev/null
+++ b/kompendium-core/src/test/resources/free_form_field.json
@@ -0,0 +1,70 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "Test API",
+    "version": "1.33.7",
+    "description": "An amazing, fully-ish 😉 generated API spec",
+    "termsOfService": "https://example.com",
+    "contact": {
+      "name": "Homer Simpson",
+      "url": "https://gph.is/1NPUDiM",
+      "email": "chunkylover53@aol.com"
+    },
+    "license": {
+      "name": "MIT",
+      "url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
+    }
+  },
+  "servers": [
+    {
+      "url": "https://myawesomeapi.com",
+      "description": "Production instance of my API"
+    },
+    {
+      "url": "https://staging.myawesomeapi.com",
+      "description": "Where the fun stuff happens"
+    }
+  ],
+  "paths": {
+    "/test/required_param": {
+      "get": {
+        "tags": [],
+        "summary": "required param",
+        "description": "Cool stuff",
+        "parameters": [],
+        "responses": {
+          "200": {
+            "description": "A successful endeavor",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/FreeFormData"
+                }
+              }
+            }
+          }
+        },
+        "deprecated": false
+      }
+    }
+  },
+  "components": {
+    "schemas": {
+      "FreeFormData": {
+        "properties": {
+          "data": {
+            "additionalProperties": true,
+            "type": "object"
+          }
+        },
+        "required": [
+          "data"
+        ],
+        "type": "object"
+      }
+    },
+    "securitySchemes": {}
+  },
+  "security": [],
+  "tags": []
+}
diff --git a/kompendium-core/src/test/resources/free_form_object.json b/kompendium-core/src/test/resources/free_form_object.json
index 8eae097a34..0531378069 100644
--- a/kompendium-core/src/test/resources/free_form_object.json
+++ b/kompendium-core/src/test/resources/free_form_object.json
@@ -38,7 +38,8 @@
             "content": {
               "application/json": {
                 "schema": {
-                  "$ref": "#/components/schemas/FreeFormData"
+                  "additionalProperties": true,
+                  "type": "object"
                 }
               }
             }
@@ -49,20 +50,7 @@
     }
   },
   "components": {
-    "schemas": {
-      "FreeFormData": {
-        "properties": {
-          "data": {
-            "additionalProperties": true,
-            "type": "object"
-          }
-        },
-        "required": [
-          "data"
-        ],
-        "type": "object"
-      }
-    },
+    "schemas": {},
     "securitySchemes": {}
   },
   "security": [],
diff --git a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt
index 09f4ed8c02..5cee52a81e 100644
--- a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt
+++ b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt
@@ -141,6 +141,9 @@ data class FreeFormData(
   val data: JsonElement
 )
 
+@FreeFormObject
+object AnythingGoesMan
+
 data class MinMaxFreeForm(
   @FreeFormObject
   @MinProperties(5)
diff --git a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt
index 2249346f4b..e10b752dc3 100644
--- a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt
+++ b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestResponseInfo.kt
@@ -258,7 +258,13 @@ object TestResponseInfo {
     responseInfo = simpleOkResponse()
   )
 
-  val freeFormObject = GetInfo<Unit, FreeFormData>(
+  val freeFormField = GetInfo<Unit, FreeFormData>(
+    summary = "required param",
+    description = "Cool stuff",
+    responseInfo = simpleOkResponse()
+  )
+
+  val freeFormObject = GetInfo<Unit, AnythingGoesMan>(
     summary = "required param",
     description = "Cool stuff",
     responseInfo = simpleOkResponse()