Repository: bkbnio/kompendium
Pull request #179 (2022-02-08 16:35:31)

chore: handler refactor
# Description

Updates a bunch of internals to make the code more modular and readable 

Closes # (issue)

## Type of change

Please delete options that are not relevant.

- [ ] 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)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes.

# Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] I have updated the CHANGELOG and bumped the version
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 68eee7875..6b97d5e15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ### Added
 
 ### Changed
+- Cleaned up and broke out handlers into separate classes
 
 ### Remove
 
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt
index 19c0eba28..7c7a527c0 100644
--- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kompendium.kt
@@ -18,12 +18,12 @@ class Kompendium(val config: Configuration) {
   class Configuration {
     lateinit var spec: OpenApiSpec
 
-    var cache: SchemaMap = emptyMap()
+    var cache: SchemaMap = mutableMapOf()
     var specRoute = "/openapi.json"
 
     // TODO Add tests for this!!
     fun addCustomTypeSchema(clazz: KClass<*>, schema: TypedSchema) {
-      cache = cache.plus(clazz.simpleName!! to schema)
+      cache[clazz.simpleName!!] = schema
     }
   }
 
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/KompendiumPreFlight.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/KompendiumPreFlight.kt
index ee094e201..a693a4e78 100644
--- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/KompendiumPreFlight.kt
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/KompendiumPreFlight.kt
@@ -43,9 +43,9 @@ object KompendiumPreFlight {
   }
 
   fun addToCache(paramType: KType, requestType: KType, responseType: KType, feature: Kompendium) {
-    feature.config.cache = Kontent.generateKontent(requestType, feature.config.cache)
-    feature.config.cache = Kontent.generateKontent(responseType, feature.config.cache)
-    feature.config.cache = Kontent.generateKontent(paramType, feature.config.cache)
+    Kontent.generateKontent(requestType, feature.config.cache)
+    Kontent.generateKontent(responseType, feature.config.cache)
+    Kontent.generateKontent(paramType, feature.config.cache)
     feature.updateReferences()
   }
 
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kontent.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kontent.kt
index b1289f08d..7f4883eb3 100644
--- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kontent.kt
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/Kontent.kt
@@ -1,49 +1,17 @@
 package io.bkbn.kompendium.core
 
-import io.bkbn.kompendium.annotations.Field
-import io.bkbn.kompendium.annotations.FreeFormObject
-import io.bkbn.kompendium.annotations.Referenced
-import io.bkbn.kompendium.annotations.UndeclaredField
-import io.bkbn.kompendium.annotations.constraint.Format
-import io.bkbn.kompendium.annotations.constraint.MaxItems
-import io.bkbn.kompendium.annotations.constraint.MaxLength
-import io.bkbn.kompendium.annotations.constraint.MaxProperties
-import io.bkbn.kompendium.annotations.constraint.Maximum
-import io.bkbn.kompendium.annotations.constraint.MinItems
-import io.bkbn.kompendium.annotations.constraint.MinLength
-import io.bkbn.kompendium.annotations.constraint.MinProperties
-import io.bkbn.kompendium.annotations.constraint.Minimum
-import io.bkbn.kompendium.annotations.constraint.MultipleOf
-import io.bkbn.kompendium.annotations.constraint.Pattern
-import io.bkbn.kompendium.annotations.constraint.UniqueItems
 import io.bkbn.kompendium.core.metadata.SchemaMap
-import io.bkbn.kompendium.core.metadata.TypeMap
-import io.bkbn.kompendium.core.util.Helpers.genericNameAdapter
-import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug
-import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
+import io.bkbn.kompendium.core.schema.CollectionHandler
+import io.bkbn.kompendium.core.schema.EnumHandler
+import io.bkbn.kompendium.core.schema.MapHandler
+import io.bkbn.kompendium.core.schema.ObjectHandler
 import io.bkbn.kompendium.core.util.Helpers.logged
-import io.bkbn.kompendium.core.util.Helpers.toNumber
-import io.bkbn.kompendium.oas.schema.AnyOfSchema
-import io.bkbn.kompendium.oas.schema.ArraySchema
-import io.bkbn.kompendium.oas.schema.ComponentSchema
-import io.bkbn.kompendium.oas.schema.DictionarySchema
-import io.bkbn.kompendium.oas.schema.EnumSchema
 import io.bkbn.kompendium.oas.schema.FormattedSchema
-import io.bkbn.kompendium.oas.schema.FreeFormSchema
-import io.bkbn.kompendium.oas.schema.ObjectSchema
-import io.bkbn.kompendium.oas.schema.ReferencedSchema
 import io.bkbn.kompendium.oas.schema.SimpleSchema
 import kotlin.reflect.KClass
-import kotlin.reflect.KClassifier
-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.isSubclassOf
-import kotlin.reflect.full.memberProperties
-import kotlin.reflect.full.primaryConstructor
-import kotlin.reflect.jvm.javaField
 import kotlin.reflect.typeOf
 import org.slf4j.LoggerFactory
 import java.math.BigDecimal
@@ -65,10 +33,10 @@ object Kontent {
    */
   @OptIn(ExperimentalStdlibApi::class)
   inline fun <reified T> generateKontent(
-    cache: SchemaMap = emptyMap()
-  ): SchemaMap {
+    cache: SchemaMap = mutableMapOf()
+  ) {
     val kontentType = typeOf<T>()
-    return generateKTypeKontent(kontentType, cache)
+    generateKTypeKontent(kontentType, cache)
   }
 
   /**
@@ -79,13 +47,11 @@ object Kontent {
    */
   fun generateKontent(
     type: KType,
-    cache: SchemaMap = emptyMap()
-  ): SchemaMap {
-    var newCache = cache
+    cache: SchemaMap = mutableMapOf()
+  ) {
     gatherSubTypes(type).forEach {
-      newCache = generateKTypeKontent(it, newCache)
+      generateKTypeKontent(it, cache)
     }
-    return newCache
   }
 
   private fun gatherSubTypes(type: KType): List<KType> {
@@ -106,371 +72,27 @@ object Kontent {
    */
   fun generateKTypeKontent(
     type: KType,
-    cache: SchemaMap = emptyMap(),
-  ): SchemaMap = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
+    cache: SchemaMap = mutableMapOf(),
+  ) = logged(object {}.javaClass.enclosingMethod.name, mapOf("cache" to cache)) {
     logger.debug("Parsing Kontent of $type")
     when (val clazz = type.classifier as KClass<*>) {
       Unit::class -> cache
-      Int::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int32", "integer"))
-      Long::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer"))
-      Double::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number"))
-      Float::class -> cache.plus(clazz.simpleName!! to FormattedSchema("float", "number"))
-      String::class -> cache.plus(clazz.simpleName!! to SimpleSchema("string"))
-      Boolean::class -> cache.plus(clazz.simpleName!! to SimpleSchema("boolean"))
-      UUID::class -> cache.plus(clazz.simpleName!! to FormattedSchema("uuid", "string"))
-      BigDecimal::class -> cache.plus(clazz.simpleName!! to FormattedSchema("double", "number"))
-      BigInteger::class -> cache.plus(clazz.simpleName!! to FormattedSchema("int64", "integer"))
-      ByteArray::class -> cache.plus(clazz.simpleName!! to FormattedSchema("byte", "string"))
+      Int::class -> cache[clazz.simpleName!!] = FormattedSchema("int32", "integer")
+      Long::class -> cache[clazz.simpleName!!] = FormattedSchema("int64", "integer")
+      Double::class -> cache[clazz.simpleName!!] = FormattedSchema("double", "number")
+      Float::class -> cache[clazz.simpleName!!] = FormattedSchema("float", "number")
+      String::class -> cache[clazz.simpleName!!] = SimpleSchema("string")
+      Boolean::class -> cache[clazz.simpleName!!] = SimpleSchema("boolean")
+      UUID::class -> cache[clazz.simpleName!!] = FormattedSchema("uuid", "string")
+      BigDecimal::class -> cache[clazz.simpleName!!] = FormattedSchema("double", "number")
+      BigInteger::class -> cache[clazz.simpleName!!] = FormattedSchema("int64", "integer")
+      ByteArray::class -> cache[clazz.simpleName!!] = FormattedSchema("byte", "string")
       else -> when {
-        clazz.isSubclassOf(Collection::class) -> handleCollectionType(type, clazz, cache)
-        clazz.isSubclassOf(Enum::class) -> handleEnumType(clazz, cache)
-        clazz.isSubclassOf(Map::class) -> handleMapType(type, clazz, cache)
-        else -> handleComplexType(type, clazz, cache)
+        clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, clazz, cache)
+        clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz, cache)
+        clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, clazz, cache)
+        else -> ObjectHandler.handle(type, clazz, cache)
       }
     }
   }
-
-  /**
-   * In the event of an object type, this method will parse out individual fields to recursively aggregate object map.
-   * @param clazz Class of the object to analyze
-   * @param cache Existing schema map to append to
-   */
-  @Suppress("LongMethod", "ComplexMethod")
-  private fun handleComplexType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
-    // This needs to be simple because it will be stored under its appropriate reference component implicitly
-    val slug = type.getSimpleSlug()
-    // Only analyze if component has not already been stored in the cache
-    return when (cache.containsKey(slug)) {
-      true -> {
-        logger.debug("Cache already contains $slug, returning cache untouched")
-        cache
-      }
-      false -> {
-        logger.debug("$slug was not found in cache, generating now")
-        var newCache = cache
-        // If referenced, add tie from simple slug to schema slug
-        if (clazz.hasAnnotation<Referenced>()) {
-          newCache = newCache.plus(type.getSimpleSlug() to ReferencedSchema(type.getReferenceSlug()))
-        }
-        // Grabs any type parameters mapped to the corresponding type argument(s)
-        val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap()
-        // associates each member with a Pair of prop name to property schema
-        val fieldMap = clazz.memberProperties.associate { prop ->
-          logger.debug("Analyzing $prop in class $clazz")
-          // Grab the field of the current property
-          val field = prop.javaField?.type?.kotlin ?: error("Unable to parse field type from $prop")
-          // Short circuit if data is free form
-          val freeForm = prop.findAnnotation<FreeFormObject>()
-          var name = prop.name
-
-          // todo add method to clean up
-          when (freeForm) {
-            null -> {
-              val baseType = scanForGeneric(typeMap, prop)
-              val baseClazz = baseType.classifier as KClass<*>
-              val allTypes = scanForSealed(baseClazz, baseType)
-              newCache = updateCache(newCache, field, allTypes)
-              var propSchema = constructComponentSchema(
-                typeMap = typeMap,
-                prop = prop,
-                fieldClazz = field,
-                clazz = baseClazz,
-                type = baseType,
-                cache = newCache
-              )
-              // todo move to helper
-              prop.findAnnotation<Field>()?.let { fieldOverrides ->
-                if (fieldOverrides.description.isNotBlank()) {
-                  propSchema = propSchema.setDescription(fieldOverrides.description)
-                }
-                if (fieldOverrides.name.isNotBlank()) {
-                  name = fieldOverrides.name
-                }
-              }
-              Pair(name, propSchema)
-            }
-            else -> {
-              val minProperties = prop.findAnnotation<MinProperties>()
-              val maxProperties = prop.findAnnotation<MaxProperties>()
-              val schema =
-                FreeFormSchema(minProperties = minProperties?.properties, maxProperties = maxProperties?.properties)
-              Pair(name, schema)
-            }
-          }
-        }
-        logger.debug("Looking for undeclared fields")
-        val undeclaredFieldMap = clazz.annotations.filterIsInstance<UndeclaredField>().associate {
-          val undeclaredType = it.clazz.createType()
-          newCache = generateKontent(undeclaredType, newCache)
-          it.field to newCache[undeclaredType.getSimpleSlug()]!!
-        }
-        logger.debug("$slug contains $fieldMap")
-        var schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
-        val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
-        // todo de-dup this logic
-        if (requiredParams.isNotEmpty()) {
-          schema = schema.copy(required = requiredParams.map { param ->
-            clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
-              ?.let { field -> field.name.ifBlank { param.name!! } }
-              ?: param.name!!
-          })
-        }
-        logger.debug("$slug schema: $schema")
-        newCache.plus(slug to schema)
-      }
-    }
-  }
-
-  /**
-   * Takes the type information provided and adds any missing data to the schema map
-   */
-  private fun updateCache(cache: SchemaMap, clazz: KClass<*>, types: List<KType>): SchemaMap {
-    var newCache = cache
-    if (!cache.containsKey(clazz.simpleName)) {
-      logger.debug("Cache was missing ${clazz.simpleName}, adding now")
-      types.forEach {
-        newCache = generateKTypeKontent(it, newCache)
-      }
-    }
-    return newCache
-  }
-
-  /**
-   * Scans a class for sealed subclasses.  If found, returns a list with all children.  Otherwise, returns
-   * the base type
-   */
-  private fun scanForSealed(clazz: KClass<*>, type: KType): List<KType> = if (clazz.isSealed) {
-    clazz.sealedSubclasses.map { it.createType(type.arguments) }
-  } else {
-    listOf(type)
-  }
-
-  /**
-   * Yoinks any generic types from the type map should the field be a generic
-   */
-  private fun scanForGeneric(typeMap: TypeMap, prop: KProperty1<*, *>): KType =
-    if (typeMap.containsKey(prop.returnType.classifier)) {
-      logger.debug("Generic type detected")
-      typeMap[prop.returnType.classifier]?.type!!
-    } else {
-      prop.returnType
-    }
-
-  /**
-   * Constructs a [ComponentSchema]
-   */
-  private fun constructComponentSchema(
-    typeMap: TypeMap,
-    clazz: KClass<*>,
-    fieldClazz: KClass<*>,
-    prop: KProperty1<*, *>,
-    type: KType,
-    cache: SchemaMap
-  ): ComponentSchema =
-    when (typeMap.containsKey(prop.returnType.classifier)) {
-      true -> handleGenericProperty(typeMap, clazz, type, prop.returnType.classifier, cache)
-      false -> handleStandardProperty(clazz, fieldClazz, prop, type, cache)
-    }.scanForConstraints(clazz, prop)
-
-  private fun ComponentSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ComponentSchema =
-    when (this) {
-      is AnyOfSchema -> AnyOfSchema(anyOf.map { it.scanForConstraints(clazz, prop) })
-      is ArraySchema -> scanForConstraints(prop)
-      is DictionarySchema -> this // TODO Anything here?
-      is EnumSchema -> scanForConstraints(prop)
-      is FormattedSchema -> scanForConstraints(prop)
-      is FreeFormSchema -> this // todo anything here?
-      is ObjectSchema -> scanForConstraints(clazz, prop)
-      is SimpleSchema -> scanForConstraints(prop)
-      is ReferencedSchema -> this // todo anything here?
-    }
-
-  private fun ArraySchema.scanForConstraints(prop: KProperty1<*, *>): ArraySchema {
-    val minItems = prop.findAnnotation<MinItems>()
-    val maxItems = prop.findAnnotation<MaxItems>()
-    val uniqueItems = prop.findAnnotation<UniqueItems>()
-
-    return this.copy(
-      minItems = minItems?.items,
-      maxItems = maxItems?.items,
-      uniqueItems = uniqueItems?.let { true }
-    )
-  }
-
-  private fun EnumSchema.scanForConstraints(prop: KProperty1<*, *>): EnumSchema {
-    if (prop.returnType.isMarkedNullable) {
-      return this.copy(nullable = true)
-    }
-
-    return this
-  }
-
-  private fun FormattedSchema.scanForConstraints(prop: KProperty1<*, *>): FormattedSchema {
-    val minimum = prop.findAnnotation<Minimum>()
-    val maximum = prop.findAnnotation<Maximum>()
-    val multipleOf = prop.findAnnotation<MultipleOf>()
-
-    var schema = this
-
-    if (prop.returnType.isMarkedNullable) {
-      schema = schema.copy(nullable = true)
-    }
-
-    return schema.copy(
-      minimum = minimum?.min?.toNumber(),
-      maximum = maximum?.max?.toNumber(),
-      exclusiveMinimum = minimum?.exclusive,
-      exclusiveMaximum = maximum?.exclusive,
-      multipleOf = multipleOf?.multiple?.toNumber(),
-    )
-  }
-
-  private fun SimpleSchema.scanForConstraints(prop: KProperty1<*, *>): SimpleSchema {
-    val minLength = prop.findAnnotation<MinLength>()
-    val maxLength = prop.findAnnotation<MaxLength>()
-    val pattern = prop.findAnnotation<Pattern>()
-    val format = prop.findAnnotation<Format>()
-
-    var schema = this
-
-    if (prop.returnType.isMarkedNullable) {
-      schema = schema.copy(nullable = true)
-    }
-
-    return schema.copy(
-      minLength = minLength?.length,
-      maxLength = maxLength?.length,
-      pattern = pattern?.pattern,
-      format = format?.format
-    )
-  }
-
-  private fun ObjectSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ObjectSchema {
-    val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
-    var schema = this
-
-    // todo dedup this
-    if (requiredParams.isNotEmpty()) {
-      schema = schema.copy(required = requiredParams.map { param ->
-        clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
-          ?.let { field -> field.name.ifBlank { param.name!! } }
-          ?: param.name!!
-      })
-    }
-
-    if (prop.returnType.isMarkedNullable) {
-      schema = schema.copy(nullable = true)
-    }
-
-    return schema
-  }
-
-  /**
-   * If a field has no type parameters, build its [ComponentSchema] without referencing the [TypeMap]
-   */
-  private fun handleStandardProperty(
-    clazz: KClass<*>,
-    fieldClazz: KClass<*>,
-    prop: KProperty1<*, *>,
-    type: KType,
-    cache: SchemaMap
-  ): ComponentSchema = if (clazz.isSealed) {
-    val refs = clazz.sealedSubclasses
-      .map { it.createType(type.arguments) }
-      .map { cache[it.getSimpleSlug()] ?: error("$it not found in cache") }
-    AnyOfSchema(refs)
-  } else {
-    val slug = fieldClazz.getSimpleSlug(prop)
-    cache[slug] ?: error("$slug not found in cache")
-  }
-
-  /**
-   * If a field has type parameters, leverage the constructed [TypeMap] to construct the [ComponentSchema]
-   */
-  private fun handleGenericProperty(
-    typeMap: TypeMap,
-    clazz: KClass<*>,
-    type: KType,
-    classifier: KClassifier?,
-    cache: SchemaMap
-  ): ComponentSchema = if (clazz.isSealed) {
-    val refs = clazz.sealedSubclasses
-      .map { it.createType(type.arguments) }
-      .map { it.getSimpleSlug() }
-      .map { cache[it] ?: error("$it not available in cache") }
-    AnyOfSchema(refs)
-  } else {
-    val slug = typeMap[classifier]?.type!!.getSimpleSlug()
-    cache[slug] ?: error("$slug not found in cache")
-  }
-
-  /**
-   * Handler for when an [Enum] is encountered
-   * @param clazz Class of the object to analyze
-   * @param cache Existing schema map to append to
-   */
-  private fun handleEnumType(clazz: KClass<*>, cache: SchemaMap): SchemaMap {
-    val options = clazz.java.enumConstants.map { it.toString() }.toSet()
-    return cache.plus(clazz.simpleName!! to EnumSchema(options))
-  }
-
-  /**
-   * Handler for when a [Map] is encountered
-   * @param type Map type information
-   * @param clazz Map class information
-   * @param cache Existing schema map to append to
-   */
-  private fun handleMapType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
-    logger.debug("Map detected for $type, generating schema and appending to cache")
-    val (keyType, valType) = type.arguments.map { it.type }
-    logger.debug("Obtained map types -> key: $keyType and value: $valType")
-    if (keyType?.classifier != String::class) {
-      error("Invalid Map $type: OpenAPI dictionaries must have keys of type String")
-    }
-    var updatedCache = generateKTypeKontent(valType!!, cache)
-    val valClass = valType.classifier as KClass<*>
-    val valClassName = valClass.simpleName
-    val referenceName = genericNameAdapter(type, clazz)
-    val valueReference = when (valClass.isSealed) {
-      true -> {
-        val subTypes = gatherSubTypes(valType)
-        AnyOfSchema(subTypes.map {
-          updatedCache = generateKTypeKontent(it, updatedCache)
-          updatedCache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
-        })
-      }
-      false -> updatedCache[valClassName] ?: error("$valClassName not found")
-    }
-    val schema = DictionarySchema(additionalProperties = valueReference)
-    updatedCache = generateKontent(valType, updatedCache)
-    return updatedCache.plus(referenceName to schema)
-  }
-
-  /**
-   * Handler for when a [Collection] is encountered
-   * @param type Collection type information
-   * @param clazz Collection class information
-   * @param cache Existing schema map to append to
-   */
-  private fun handleCollectionType(type: KType, clazz: KClass<*>, cache: SchemaMap): SchemaMap {
-    logger.debug("Collection detected for $type, generating schema and appending to cache")
-    val collectionType = type.arguments.first().type!!
-    val collectionClass = collectionType.classifier as KClass<*>
-    logger.debug("Obtained collection class: $collectionClass")
-    val referenceName = genericNameAdapter(type, clazz)
-    var updatedCache = generateKTypeKontent(collectionType, cache)
-    val valueReference = when (collectionClass.isSealed) {
-      true -> {
-        val subTypes = gatherSubTypes(collectionType)
-        AnyOfSchema(subTypes.map {
-          updatedCache = generateKTypeKontent(it, cache)
-          updatedCache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
-        })
-      }
-      false -> updatedCache[collectionClass.simpleName] ?: error("${collectionClass.simpleName} not found")
-    }
-    val schema = ArraySchema(items = valueReference)
-    updatedCache = generateKontent(collectionType, cache)
-    return updatedCache.plus(referenceName to schema)
-  }
 }
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/constraint/ConstraintScanner.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/constraint/ConstraintScanner.kt
new file mode 100644
index 000000000..31ff7f06e
--- /dev/null
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/constraint/ConstraintScanner.kt
@@ -0,0 +1,122 @@
+package io.bkbn.kompendium.core.constraint
+
+import io.bkbn.kompendium.annotations.Field
+import io.bkbn.kompendium.annotations.constraint.Format
+import io.bkbn.kompendium.annotations.constraint.MaxItems
+import io.bkbn.kompendium.annotations.constraint.MaxLength
+import io.bkbn.kompendium.annotations.constraint.Maximum
+import io.bkbn.kompendium.annotations.constraint.MinItems
+import io.bkbn.kompendium.annotations.constraint.MinLength
+import io.bkbn.kompendium.annotations.constraint.Minimum
+import io.bkbn.kompendium.annotations.constraint.MultipleOf
+import io.bkbn.kompendium.annotations.constraint.Pattern
+import io.bkbn.kompendium.annotations.constraint.UniqueItems
+import io.bkbn.kompendium.core.util.Helpers.toNumber
+import io.bkbn.kompendium.oas.schema.AnyOfSchema
+import io.bkbn.kompendium.oas.schema.ArraySchema
+import io.bkbn.kompendium.oas.schema.ComponentSchema
+import io.bkbn.kompendium.oas.schema.DictionarySchema
+import io.bkbn.kompendium.oas.schema.EnumSchema
+import io.bkbn.kompendium.oas.schema.FormattedSchema
+import io.bkbn.kompendium.oas.schema.FreeFormSchema
+import io.bkbn.kompendium.oas.schema.ObjectSchema
+import io.bkbn.kompendium.oas.schema.ReferencedSchema
+import io.bkbn.kompendium.oas.schema.SimpleSchema
+import kotlin.reflect.KClass
+import kotlin.reflect.KProperty1
+import kotlin.reflect.full.findAnnotation
+import kotlin.reflect.full.memberProperties
+import kotlin.reflect.full.primaryConstructor
+
+fun ComponentSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ComponentSchema =
+  when (this) {
+    is AnyOfSchema -> AnyOfSchema(anyOf.map { it.scanForConstraints(clazz, prop) })
+    is ArraySchema -> scanForConstraints(prop)
+    is DictionarySchema -> this // TODO Anything here?
+    is EnumSchema -> scanForConstraints(prop)
+    is FormattedSchema -> scanForConstraints(prop)
+    is FreeFormSchema -> this // todo anything here?
+    is ObjectSchema -> scanForConstraints(clazz, prop)
+    is SimpleSchema -> scanForConstraints(prop)
+    is ReferencedSchema -> this // todo anything here?
+  }
+
+fun ArraySchema.scanForConstraints(prop: KProperty1<*, *>): ArraySchema {
+  val minItems = prop.findAnnotation<MinItems>()
+  val maxItems = prop.findAnnotation<MaxItems>()
+  val uniqueItems = prop.findAnnotation<UniqueItems>()
+
+  return this.copy(
+    minItems = minItems?.items,
+    maxItems = maxItems?.items,
+    uniqueItems = uniqueItems?.let { true }
+  )
+}
+
+fun EnumSchema.scanForConstraints(prop: KProperty1<*, *>): EnumSchema {
+  if (prop.returnType.isMarkedNullable) {
+    return this.copy(nullable = true)
+  }
+
+  return this
+}
+
+fun FormattedSchema.scanForConstraints(prop: KProperty1<*, *>): FormattedSchema {
+  val minimum = prop.findAnnotation<Minimum>()
+  val maximum = prop.findAnnotation<Maximum>()
+  val multipleOf = prop.findAnnotation<MultipleOf>()
+
+  var schema = this
+
+  if (prop.returnType.isMarkedNullable) {
+    schema = schema.copy(nullable = true)
+  }
+
+  return schema.copy(
+    minimum = minimum?.min?.toNumber(),
+    maximum = maximum?.max?.toNumber(),
+    exclusiveMinimum = minimum?.exclusive,
+    exclusiveMaximum = maximum?.exclusive,
+    multipleOf = multipleOf?.multiple?.toNumber(),
+  )
+}
+
+fun SimpleSchema.scanForConstraints(prop: KProperty1<*, *>): SimpleSchema {
+  val minLength = prop.findAnnotation<MinLength>()
+  val maxLength = prop.findAnnotation<MaxLength>()
+  val pattern = prop.findAnnotation<Pattern>()
+  val format = prop.findAnnotation<Format>()
+
+  var schema = this
+
+  if (prop.returnType.isMarkedNullable) {
+    schema = schema.copy(nullable = true)
+  }
+
+  return schema.copy(
+    minLength = minLength?.length,
+    maxLength = maxLength?.length,
+    pattern = pattern?.pattern,
+    format = format?.format
+  )
+}
+
+fun ObjectSchema.scanForConstraints(clazz: KClass<*>, prop: KProperty1<*, *>): ObjectSchema {
+  val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
+  var schema = this
+
+  // todo dedup this
+  if (requiredParams.isNotEmpty()) {
+    schema = schema.copy(required = requiredParams.map { param ->
+      clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
+        ?.let { field -> field.name.ifBlank { param.name!! } }
+        ?: param.name!!
+    })
+  }
+
+  if (prop.returnType.isMarkedNullable) {
+    schema = schema.copy(nullable = true)
+  }
+
+  return schema
+}
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/SchemaMap.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/SchemaMap.kt
index 92889ca6e..22c016ca1 100644
--- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/SchemaMap.kt
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/metadata/SchemaMap.kt
@@ -2,4 +2,4 @@ package io.bkbn.kompendium.core.metadata
 
 import io.bkbn.kompendium.oas.schema.ComponentSchema
 
-typealias SchemaMap = Map<String, ComponentSchema>
+typealias SchemaMap = MutableMap<String, ComponentSchema>
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/parser/IMethodParser.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/parser/IMethodParser.kt
index 0650a82e6..1784aab3a 100644
--- a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/parser/IMethodParser.kt
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/parser/IMethodParser.kt
@@ -77,7 +77,7 @@ interface IMethodParser {
     exceptionInfo: Set<ExceptionInfo<*>>,
     feature: Kompendium,
   ): Map<Int, Response> = exceptionInfo.associate { info ->
-    feature.config.cache = Kontent.generateKontent(info.responseType, feature.config.cache)
+    Kontent.generateKontent(info.responseType, feature.config.cache)
     val response = Response(
       description = info.description,
       content = feature.resolveContent(info.responseType, info.mediaTypes, info.examples)
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/CollectionHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/CollectionHandler.kt
new file mode 100644
index 000000000..037759e4d
--- /dev/null
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/CollectionHandler.kt
@@ -0,0 +1,45 @@
+package io.bkbn.kompendium.core.schema
+
+import io.bkbn.kompendium.core.Kontent
+import io.bkbn.kompendium.core.Kontent.generateKTypeKontent
+import io.bkbn.kompendium.core.metadata.SchemaMap
+import io.bkbn.kompendium.core.util.Helpers
+import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
+import io.bkbn.kompendium.oas.schema.AnyOfSchema
+import io.bkbn.kompendium.oas.schema.ArraySchema
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
+import org.slf4j.LoggerFactory
+
+object CollectionHandler : SchemaHandler {
+
+  private val logger = LoggerFactory.getLogger(javaClass)
+
+  /**
+   * Handler for when a [Collection] is encountered
+   * @param type Collection type information
+   * @param clazz Collection class information
+   * @param cache Existing schema map to append to
+   */
+  override fun handle(type: KType, clazz: KClass<*>, cache: SchemaMap) {
+    logger.debug("Collection detected for $type, generating schema and appending to cache")
+    val collectionType = type.arguments.first().type!!
+    val collectionClass = collectionType.classifier as KClass<*>
+    logger.debug("Obtained collection class: $collectionClass")
+    val referenceName = Helpers.genericNameAdapter(type, clazz)
+    generateKTypeKontent(collectionType, cache)
+    val valueReference = when (collectionClass.isSealed) {
+      true -> {
+        val subTypes = gatherSubTypes(collectionType)
+        AnyOfSchema(subTypes.map {
+          generateKTypeKontent(it, cache)
+          cache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
+        })
+      }
+      false -> cache[collectionClass.simpleName] ?: error("${collectionClass.simpleName} not found")
+    }
+    val schema = ArraySchema(items = valueReference)
+    Kontent.generateKontent(collectionType, cache)
+    cache[referenceName] = schema
+  }
+}
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/EnumHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/EnumHandler.kt
new file mode 100644
index 000000000..b273df055
--- /dev/null
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/EnumHandler.kt
@@ -0,0 +1,20 @@
+package io.bkbn.kompendium.core.schema
+
+import io.bkbn.kompendium.core.metadata.SchemaMap
+import io.bkbn.kompendium.oas.schema.EnumSchema
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
+
+object EnumHandler : SchemaHandler {
+
+  /**
+   * Handler for when an [Enum] is encountered
+   * @param type Map type information
+   * @param clazz Class of the object to analyze
+   * @param cache Existing schema map to append to
+   */
+  override fun handle(type: KType, clazz: KClass<*>, cache: SchemaMap) {
+    val options = clazz.java.enumConstants.map { it.toString() }.toSet()
+    cache[clazz.simpleName!!] = EnumSchema(options)
+  }
+}
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/MapHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/MapHandler.kt
new file mode 100644
index 000000000..cfe65ada3
--- /dev/null
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/MapHandler.kt
@@ -0,0 +1,49 @@
+package io.bkbn.kompendium.core.schema
+
+import io.bkbn.kompendium.core.Kontent.generateKTypeKontent
+import io.bkbn.kompendium.core.Kontent.generateKontent
+import io.bkbn.kompendium.core.metadata.SchemaMap
+import io.bkbn.kompendium.core.util.Helpers.genericNameAdapter
+import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
+import io.bkbn.kompendium.oas.schema.AnyOfSchema
+import io.bkbn.kompendium.oas.schema.DictionarySchema
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
+import org.slf4j.LoggerFactory
+
+object MapHandler : SchemaHandler {
+
+  private val logger = LoggerFactory.getLogger(javaClass)
+
+  /**
+   * Handler for when a [Map] is encountered
+   * @param type Map type information
+   * @param clazz Map class information
+   * @param cache Existing schema map to append to
+   */
+  override fun handle(type: KType, clazz: KClass<*>, cache: SchemaMap) {
+    logger.debug("Map detected for $type, generating schema and appending to cache")
+    val (keyType, valType) = type.arguments.map { it.type }
+    logger.debug("Obtained map types -> key: $keyType and value: $valType")
+    if (keyType?.classifier != String::class) {
+      error("Invalid Map $type: OpenAPI dictionaries must have keys of type String")
+    }
+    generateKTypeKontent(valType!!, cache)
+    val valClass = valType.classifier as KClass<*>
+    val valClassName = valClass.simpleName
+    val referenceName = genericNameAdapter(type, clazz)
+    val valueReference = when (valClass.isSealed) {
+      true -> {
+        val subTypes = gatherSubTypes(valType)
+        AnyOfSchema(subTypes.map {
+          generateKTypeKontent(it, cache)
+          cache[it.getSimpleSlug()] ?: error("${it.getSimpleSlug()} not found")
+        })
+      }
+      false -> cache[valClassName] ?: error("$valClassName not found")
+    }
+    val schema = DictionarySchema(additionalProperties = valueReference)
+    generateKontent(valType, cache)
+    cache[referenceName] = schema
+  }
+}
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/ObjectHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/ObjectHandler.kt
new file mode 100644
index 000000000..f4c52ab35
--- /dev/null
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/ObjectHandler.kt
@@ -0,0 +1,223 @@
+package io.bkbn.kompendium.core.schema
+
+import io.bkbn.kompendium.annotations.Field
+import io.bkbn.kompendium.annotations.FreeFormObject
+import io.bkbn.kompendium.annotations.Referenced
+import io.bkbn.kompendium.annotations.UndeclaredField
+import io.bkbn.kompendium.annotations.constraint.MaxProperties
+import io.bkbn.kompendium.annotations.constraint.MinProperties
+import io.bkbn.kompendium.core.Kontent
+import io.bkbn.kompendium.core.Kontent.generateKontent
+import io.bkbn.kompendium.core.constraint.scanForConstraints
+import io.bkbn.kompendium.core.metadata.SchemaMap
+import io.bkbn.kompendium.core.metadata.TypeMap
+import io.bkbn.kompendium.core.util.Helpers.getReferenceSlug
+import io.bkbn.kompendium.core.util.Helpers.getSimpleSlug
+import io.bkbn.kompendium.oas.schema.AnyOfSchema
+import io.bkbn.kompendium.oas.schema.ComponentSchema
+import io.bkbn.kompendium.oas.schema.FreeFormSchema
+import io.bkbn.kompendium.oas.schema.ObjectSchema
+import io.bkbn.kompendium.oas.schema.ReferencedSchema
+import kotlin.reflect.KClass
+import kotlin.reflect.KClassifier
+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.full.primaryConstructor
+import kotlin.reflect.jvm.javaField
+import org.slf4j.LoggerFactory
+
+object ObjectHandler : SchemaHandler {
+
+  private val logger = LoggerFactory.getLogger(javaClass)
+
+  /**
+   * In the event of an object type, this method will parse out individual fields to recursively aggregate object map.
+   * @param type Map type information
+   * @param clazz Class of the object to analyze
+   * @param cache Existing schema map to append to
+   */
+  override fun handle(type: KType, clazz: KClass<*>, cache: SchemaMap) {
+    // This needs to be simple because it will be stored under its appropriate reference component implicitly
+    val slug = type.getSimpleSlug()
+    // 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 Replace with something better!
+      // If referenced, add tie from simple slug to schema slug
+      if (clazz.hasAnnotation<Referenced>()) {
+        cache[type.getSimpleSlug()] = ReferencedSchema(type.getReferenceSlug())
+      }
+      // Grabs any type parameters mapped to the corresponding type argument(s)
+      val typeMap: TypeMap = clazz.typeParameters.zip(type.arguments).toMap()
+      // associates each member with a Pair of prop name to property schema
+      val fieldMap = clazz.memberProperties.associate { prop ->
+        logger.debug("Analyzing $prop in class $clazz")
+
+        // Grab the field of the current property
+        val field = prop.javaField?.type?.kotlin ?: error("Unable to parse field type from $prop")
+
+        // Short circuit if data is free form
+        val freeForm = prop.findAnnotation<FreeFormObject>()
+        var name = prop.name
+
+        when (freeForm) {
+          null -> {
+            val bleh = handleDefault(typeMap, prop, cache)
+            bleh.first
+          }
+          else -> handleFreeForm(prop)
+        }
+      }
+      logger.debug("Looking for undeclared fields")
+      val undeclaredFieldMap = clazz.annotations.filterIsInstance<UndeclaredField>().associate {
+        val undeclaredType = it.clazz.createType()
+        generateKontent(undeclaredType, cache)
+        it.field to cache[undeclaredType.getSimpleSlug()]!!
+      }
+      logger.debug("$slug contains $fieldMap")
+      var schema = ObjectSchema(fieldMap.plus(undeclaredFieldMap))
+      val requiredParams = clazz.primaryConstructor?.parameters?.filterNot { it.isOptional } ?: emptyList()
+      // todo de-dup this logic
+      if (requiredParams.isNotEmpty()) {
+        schema = schema.copy(required = requiredParams.map { param ->
+          clazz.memberProperties.first { it.name == param.name }.findAnnotation<Field>()
+            ?.let { field -> field.name.ifBlank { param.name!! } }
+            ?: param.name!!
+        })
+      }
+      logger.debug("$slug schema: $schema")
+      cache[slug] = schema
+    }
+  }
+
+  // TODO Better type, or just make map mutable
+  private fun handleDefault(
+    typeMap: TypeMap,
+    prop: KProperty1<*, *>,
+    cache: SchemaMap
+  ): Pair<Pair<String, ComponentSchema>, SchemaMap> {
+    var name = prop.name
+    val field = prop.javaField?.type?.kotlin ?: error("Unable to parse field type from $prop")
+    val baseType = scanForGeneric(typeMap, prop)
+    val baseClazz = baseType.classifier as KClass<*>
+    val allTypes = scanForSealed(baseClazz, baseType)
+    updateCache(cache, field, allTypes)
+    var propSchema = constructComponentSchema(
+      typeMap = typeMap,
+      prop = prop,
+      fieldClazz = field,
+      clazz = baseClazz,
+      type = baseType,
+      cache = cache
+    )
+    // todo move to helper
+    prop.findAnnotation<Field>()?.let { fieldOverrides ->
+      if (fieldOverrides.description.isNotBlank()) {
+        propSchema = propSchema.setDescription(fieldOverrides.description)
+      }
+      if (fieldOverrides.name.isNotBlank()) {
+        name = fieldOverrides.name
+      }
+    }
+    return Pair(Pair(name, propSchema), cache)
+  }
+
+  private fun handleFreeForm(prop: KProperty1<*, *>): Pair<String, FreeFormSchema> {
+    val minProperties = prop.findAnnotation<MinProperties>()
+    val maxProperties = prop.findAnnotation<MaxProperties>()
+    val schema = FreeFormSchema(
+      minProperties = minProperties?.properties,
+      maxProperties = maxProperties?.properties
+    )
+    return Pair(prop.name, schema)
+  }
+
+  /**
+   * Yoinks any generic types from the type map should the field be a generic
+   */
+  private fun scanForGeneric(typeMap: TypeMap, prop: KProperty1<*, *>): KType =
+    if (typeMap.containsKey(prop.returnType.classifier)) {
+      logger.debug("Generic type detected")
+      typeMap[prop.returnType.classifier]?.type!!
+    } else {
+      prop.returnType
+    }
+
+  /**
+   * Scans a class for sealed subclasses.  If found, returns a list with all children.  Otherwise, returns
+   * the base type
+   */
+  private fun scanForSealed(clazz: KClass<*>, type: KType): List<KType> = if (clazz.isSealed) {
+    clazz.sealedSubclasses.map { it.createType(type.arguments) }
+  } else {
+    listOf(type)
+  }
+
+  /**
+   * Takes the type information provided and adds any missing data to the schema map
+   */
+  private fun updateCache(cache: SchemaMap, clazz: KClass<*>, types: List<KType>) {
+    if (!cache.containsKey(clazz.simpleName)) {
+      logger.debug("Cache was missing ${clazz.simpleName}, adding now")
+      types.forEach {
+        Kontent.generateKTypeKontent(it, cache)
+      }
+    }
+  }
+
+  private fun constructComponentSchema(
+    typeMap: TypeMap,
+    clazz: KClass<*>,
+    fieldClazz: KClass<*>,
+    prop: KProperty1<*, *>,
+    type: KType,
+    cache: SchemaMap
+  ): ComponentSchema =
+    when (typeMap.containsKey(prop.returnType.classifier)) {
+      true -> handleGenericProperty(typeMap, clazz, type, prop.returnType.classifier, cache)
+      false -> handleStandardProperty(clazz, fieldClazz, prop, type, cache)
+    }.scanForConstraints(clazz, prop)
+
+  /**
+   * If a field has type parameters, leverage the constructed [TypeMap] to construct the [ComponentSchema]
+   */
+  private fun handleGenericProperty(
+    typeMap: TypeMap,
+    clazz: KClass<*>,
+    type: KType,
+    classifier: KClassifier?,
+    cache: SchemaMap
+  ): ComponentSchema = if (clazz.isSealed) {
+    val refs = clazz.sealedSubclasses
+      .map { it.createType(type.arguments) }
+      .map { it.getSimpleSlug() }
+      .map { cache[it] ?: error("$it not available in cache") }
+    AnyOfSchema(refs)
+  } else {
+    val slug = typeMap[classifier]?.type!!.getSimpleSlug()
+    cache[slug] ?: error("$slug not found in cache")
+  }
+
+  /**
+   * If a field has no type parameters, build its [ComponentSchema] without referencing the [TypeMap]
+   */
+  private fun handleStandardProperty(
+    clazz: KClass<*>,
+    fieldClazz: KClass<*>,
+    prop: KProperty1<*, *>,
+    type: KType,
+    cache: SchemaMap
+  ): ComponentSchema = if (clazz.isSealed) {
+    val refs = clazz.sealedSubclasses
+      .map { it.createType(type.arguments) }
+      .map { cache[it.getSimpleSlug()] ?: error("$it not found in cache") }
+    AnyOfSchema(refs)
+  } else {
+    val slug = fieldClazz.getSimpleSlug(prop)
+    cache[slug] ?: error("$slug not found in cache")
+  }
+}
diff --git a/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/SchemaHandler.kt b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/SchemaHandler.kt
new file mode 100644
index 000000000..98ba19654
--- /dev/null
+++ b/kompendium-core/src/main/kotlin/io/bkbn/kompendium/core/schema/SchemaHandler.kt
@@ -0,0 +1,21 @@
+package io.bkbn.kompendium.core.schema
+
+import io.bkbn.kompendium.core.metadata.SchemaMap
+import kotlin.reflect.KClass
+import kotlin.reflect.KType
+import kotlin.reflect.full.createType
+
+interface SchemaHandler {
+  fun handle(type: KType, clazz: KClass<*>, cache: SchemaMap)
+
+  fun gatherSubTypes(type: KType): List<KType> {
+    val classifier = type.classifier as KClass<*>
+    return if (classifier.isSealed) {
+      classifier.sealedSubclasses.map {
+        it.createType(type.arguments)
+      }
+    } else {
+      listOf(type)
+    }
+  }
+}


Test patch:
diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KontentTest.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KontentTest.kt
index c2ccf22b9..7cff10847 100644
--- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KontentTest.kt
+++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KontentTest.kt
@@ -12,6 +12,7 @@ import io.bkbn.kompendium.core.fixtures.TestSimpleWithEnums
 import io.bkbn.kompendium.core.fixtures.TestSimpleWithList
 import io.bkbn.kompendium.core.fixtures.TestSimpleWithMap
 import io.bkbn.kompendium.core.fixtures.TestWithUUID
+import io.bkbn.kompendium.core.metadata.SchemaMap
 import io.bkbn.kompendium.oas.schema.DictionarySchema
 import io.bkbn.kompendium.oas.schema.FormattedSchema
 import io.bkbn.kompendium.oas.schema.ObjectSchema
@@ -21,7 +22,6 @@ import io.kotest.matchers.maps.beEmpty
 import io.kotest.matchers.maps.shouldContainKey
 import io.kotest.matchers.maps.shouldHaveKey
 import io.kotest.matchers.maps.shouldHaveSize
-import io.kotest.matchers.maps.shouldNotHaveKey
 import io.kotest.matchers.should
 import io.kotest.matchers.shouldBe
 import io.kotest.matchers.shouldNotBe
@@ -30,144 +30,186 @@ import java.util.UUID
 class KontentTest : DescribeSpec({
   describe("Kontent analysis") {
     it("Can return an empty map when passed Unit") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<Unit>()
+      generateKontent<Unit>(cache)
 
       // assert
-      result should beEmpty()
+      cache should beEmpty()
     }
     it("Can return a single map result when analyzing a primitive") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<Long>()
+      generateKontent<Long>(cache)
 
       // assert
-      result shouldHaveSize 1
-      result["Long"] shouldBe FormattedSchema("int64", "integer")
+      cache shouldHaveSize 1
+      cache["Long"] shouldBe FormattedSchema("int64", "integer")
     }
     it("Can handle BigDecimal and BigInteger Types") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<TestBigNumberModel>()
+      generateKontent<TestBigNumberModel>(cache)
 
       // assert
-      result shouldHaveSize 3
-      result shouldContainKey TestBigNumberModel::class.simpleName!!
-      result["BigDecimal"] shouldBe FormattedSchema("double", "number")
-      result["BigInteger"] shouldBe FormattedSchema("int64", "integer")
+      cache shouldHaveSize 3
+      cache shouldContainKey TestBigNumberModel::class.simpleName!!
+      cache["BigDecimal"] shouldBe FormattedSchema("double", "number")
+      cache["BigInteger"] shouldBe FormattedSchema("int64", "integer")
     }
     it("Can handle ByteArray type") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<TestByteArrayModel>()
+      generateKontent<TestByteArrayModel>(cache)
 
       // assert
-      result shouldHaveSize 2
-      result shouldContainKey TestByteArrayModel::class.simpleName!!
-      result["ByteArray"] shouldBe FormattedSchema("byte", "string")
+      cache shouldHaveSize 2
+      cache shouldContainKey TestByteArrayModel::class.simpleName!!
+      cache["ByteArray"] shouldBe FormattedSchema("byte", "string")
     }
     it("Allows objects to reference their base type in the cache") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<TestSimpleModel>()
+      generateKontent<TestSimpleModel>(cache)
 
       // assert
-      result shouldNotBe null
-      result shouldHaveSize 3
-      result shouldContainKey TestSimpleModel::class.simpleName!!
+      cache shouldNotBe null
+      cache shouldHaveSize 3
+      cache shouldContainKey TestSimpleModel::class.simpleName!!
     }
     it("Can generate cache for nested object types") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<TestNestedModel>()
+      generateKontent<TestNestedModel>(cache)
 
       // assert
-      result shouldNotBe null
-      result shouldHaveSize 4
-      result shouldContainKey TestNestedModel::class.simpleName!!
-      result shouldContainKey TestSimpleModel::class.simpleName!!
+      cache shouldNotBe null
+      cache shouldHaveSize 4
+      cache shouldContainKey TestNestedModel::class.simpleName!!
+      cache shouldContainKey TestSimpleModel::class.simpleName!!
     }
     it("Does not repeat generation for cached items") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // arrange
       val clazz = TestNestedModel::class
-      val initialCache = generateKontent<TestNestedModel>()
+      generateKontent<TestNestedModel>(cache)
 
       // act
-      val result = generateKontent<TestSimpleModel>(initialCache)
+      generateKontent<TestSimpleModel>(cache)
 
       // assert TODO Spy to check invocation count?
-      result shouldNotBe null
-      result shouldHaveSize 4
-      result shouldContainKey clazz.simpleName!!
-      result shouldContainKey TestSimpleModel::class.simpleName!!
+      cache shouldNotBe null
+      cache shouldHaveSize 4
+      cache shouldContainKey clazz.simpleName!!
+      cache shouldContainKey TestSimpleModel::class.simpleName!!
     }
     it("allows for generation of enum types") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<TestSimpleWithEnums>()
+      generateKontent<TestSimpleWithEnums>(cache)
 
       // assert
-      result shouldNotBe null
-      result shouldHaveSize 3
-      result shouldContainKey TestSimpleWithEnums::class.simpleName!!
+      cache shouldNotBe null
+      cache shouldHaveSize 3
+      cache shouldContainKey TestSimpleWithEnums::class.simpleName!!
     }
     it("Allows for generation of map fields") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<TestSimpleWithMap>()
+      generateKontent<TestSimpleWithMap>(cache)
 
       // assert
-      result shouldNotBe null
-      result shouldHaveSize 5
-      result shouldContainKey "Map-String-TestSimpleModel"
-      result shouldContainKey TestSimpleWithMap::class.simpleName!!
-      result[TestSimpleWithMap::class.simpleName] as ObjectSchema shouldNotBe null // TODO Improve
+      cache shouldNotBe null
+      cache shouldHaveSize 5
+      cache shouldContainKey "Map-String-TestSimpleModel"
+      cache shouldContainKey TestSimpleWithMap::class.simpleName!!
+      cache[TestSimpleWithMap::class.simpleName] as ObjectSchema shouldNotBe null // TODO Improve
     }
     it("Throws an error if a map is of an invalid type") {
       // assert
       shouldThrow<IllegalStateException> { generateKontent<TestInvalidMap>() }
     }
     it("Can generate for collection fields") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<TestSimpleWithList>()
+      generateKontent<TestSimpleWithList>(cache)
 
       // assert
-      result shouldNotBe null
-      result shouldHaveSize 6
-      result shouldContainKey "List-TestSimpleModel"
-      result shouldContainKey TestSimpleWithList::class.simpleName!!
+      cache shouldNotBe null
+      cache shouldHaveSize 6
+      cache shouldContainKey "List-TestSimpleModel"
+      cache shouldContainKey TestSimpleWithList::class.simpleName!!
     }
     it("Can parse an enum list as a field") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<TestSimpleWithEnumList>()
+      generateKontent<TestSimpleWithEnumList>(cache)
 
       // assert
-      result shouldNotBe null
-      result shouldHaveSize 4
-      result shouldHaveKey "List-SimpleEnum"
+      cache shouldNotBe null
+      cache shouldHaveSize 4
+      cache shouldHaveKey "List-SimpleEnum"
     }
     it("Can support UUIDs") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<TestWithUUID>()
+      generateKontent<TestWithUUID>(cache)
 
       // assert
-      result shouldNotBe null
-      result shouldHaveSize 2
-      result shouldContainKey UUID::class.simpleName!!
-      result shouldContainKey TestWithUUID::class.simpleName!!
-      result[UUID::class.simpleName] as FormattedSchema shouldBe FormattedSchema("uuid", "string")
+      cache shouldNotBe null
+      cache shouldHaveSize 2
+      cache shouldContainKey UUID::class.simpleName!!
+      cache shouldContainKey TestWithUUID::class.simpleName!!
+      cache[UUID::class.simpleName] as FormattedSchema shouldBe FormattedSchema("uuid", "string")
     }
     it("Can generate a top level list response") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<List<TestSimpleModel>>()
+      generateKontent<List<TestSimpleModel>>(cache)
 
       // assert
-      result shouldNotBe null
-      result shouldHaveSize 4
-      result shouldContainKey "List-TestSimpleModel"
+      cache shouldNotBe null
+      cache shouldHaveSize 4
+      cache shouldContainKey "List-TestSimpleModel"
     }
     it("Can handle a complex type") {
+      // arrange
+      val cache: SchemaMap = mutableMapOf()
+
       // act
-      val result = generateKontent<ComplexRequest>()
+      generateKontent<ComplexRequest>(cache)
 
       // assert
-      result shouldNotBe null
-      result shouldHaveSize 7
-      result shouldContainKey "Map-String-CrazyItem"
-      result["Map-String-CrazyItem"] as DictionarySchema shouldNotBe null
+      cache shouldNotBe null
+      cache shouldHaveSize 7
+      cache shouldContainKey "Map-String-CrazyItem"
+      cache["Map-String-CrazyItem"] as DictionarySchema shouldNotBe null
     }
   }
 })