Repository: cjbooms/fabrikt
Pull request #195 (2023-04-12 09:07:20)

Add support for generating nested polymorphic models (#153)
Add support for generating nested polymorphic models 
Closes (#153)

cjbooms: I think the best thing to do is for me to merge this next week and cut a pre-release. Then test that pre-release against a couple of more complex APIs at Zalando to make sure it doesn't break anything. If all good, we can promote to maven central

Patch:
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt
index 27268c3..bd1b5d1 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt
@@ -27,6 +27,7 @@ data class ClassSettings(
 
 object PropertyUtils {
     fun PropertyInfo.addToClass(
+        modelName: String,
         type: TypeName,
         parameterizedType: TypeName,
         classBuilder: TypeSpec.Builder,
@@ -80,16 +81,17 @@ object PropertyUtils {
                 ClassSettings.PolymorphyType.SUB -> {
                     if (this is PropertyInfo.Field && isPolymorphicDiscriminator) {
                         property.addModifiers(KModifier.OVERRIDE)
-                        when (maybeDiscriminator) {
-                            is PropertyInfo.DiscriminatorKey.EnumKey ->
-                                property.initializer("%T.%L", wrappedType, maybeDiscriminator.enumKey)
-
-                            is PropertyInfo.DiscriminatorKey.StringKey ->
-                                property.initializer("%S", maybeDiscriminator.stringValue)
-
-                            else -> {
-                                property.addAnnotation(JacksonMetadata.jacksonParameterAnnotation(oasKey))
+                        val discriminators = maybeDiscriminator.getDiscriminatorMappings(modelName)
+                        if (discriminators.size == 1) {
+                            when (val discriminator = discriminators.first()) {
+                                is PropertyInfo.DiscriminatorKey.EnumKey ->
+                                    property.initializer("%T.%L", wrappedType, discriminator.enumKey)
+
+                                is PropertyInfo.DiscriminatorKey.StringKey ->
+                                    property.initializer("%S", discriminator.stringValue)
                             }
+                        } else {
+                            property.addAnnotation(JacksonMetadata.jacksonParameterAnnotation(oasKey))
                         }
                     } else {
                         if (isInherited) {
@@ -111,7 +113,8 @@ object PropertyUtils {
 
             if (this !is PropertyInfo.Field ||
                 !isPolymorphicDiscriminator ||
-                isSubTypeDiscriminatorWithNoValue(classSettings)
+                isSubTypeDiscriminatorWithNoValue(classSettings) ||
+                isSubTypeDiscriminatorWithMultipleValues(classSettings, modelName)
             ) {
                 property.initializer(name)
                 val constructorParameter: ParameterSpec.Builder = ParameterSpec.builder(name, wrappedType)
@@ -134,8 +137,20 @@ object PropertyUtils {
         classBuilder.addProperty(property.build())
     }
 
+    private fun Map<String, PropertyInfo.DiscriminatorKey>?.getDiscriminatorMappings(
+        modelName: String
+    ): List<PropertyInfo.DiscriminatorKey> =
+        this?.filter { it.value.modelName == modelName }?.map {it.value}.orEmpty()
+
     private fun PropertyInfo.Field.isSubTypeDiscriminatorWithNoValue(classType: ClassSettings) =
-        classType.polymorphyType == ClassSettings.PolymorphyType.SUB && isPolymorphicDiscriminator && maybeDiscriminator == null
+        classType.polymorphyType == ClassSettings.PolymorphyType.SUB &&
+                isPolymorphicDiscriminator &&
+                maybeDiscriminator == null
+
+    private fun PropertyInfo.Field.isSubTypeDiscriminatorWithMultipleValues(classType: ClassSettings, modelName: String) =
+        classType.polymorphyType == ClassSettings.PolymorphyType.SUB &&
+                isPolymorphicDiscriminator &&
+                maybeDiscriminator.getDiscriminatorMappings(modelName).size > 1
 
     private fun getDefaultValue(propTypeInfo: PropertyInfo, parameterizedType: TypeName): OasDefault? {
         return when (propTypeInfo) {
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
index 9bea448..5f1e71f 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -26,6 +26,7 @@ import com.cjbooms.fabrikt.model.PropertyInfo.Companion.HTTP_SETTINGS
 import com.cjbooms.fabrikt.model.PropertyInfo.Companion.topLevelProperties
 import com.cjbooms.fabrikt.model.SchemaInfo
 import com.cjbooms.fabrikt.model.SourceApi
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.getDiscriminatorForInLinedObjectUnderAllOf
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.getSuperType
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isComplexTypedAdditionalProperties
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedEnumDefinition
@@ -168,6 +169,15 @@ class JacksonModelGenerator(
     ): TypeSpec {
         val modelName = schemaInfo.name.toModelClassName()
         return when {
+            schemaInfo.schema.isPolymorphicSuperType() && schemaInfo.schema.isPolymorphicSubType(api) ->
+                polymorphicSuperSubType(
+                    modelName,
+                    properties,
+                    checkNotNull(schemaInfo.schema.getDiscriminatorForInLinedObjectUnderAllOf()),
+                    schemaInfo.schema.getSuperType(api)!!.let { SchemaInfo(it.name, it) },
+                    schemaInfo.schema.extensions,
+                    allSchemas
+            )
             schemaInfo.schema.isPolymorphicSuperType() -> polymorphicSuperType(
                 modelName,
                 properties,
@@ -342,58 +352,97 @@ class JacksonModelGenerator(
             .addMicronautReflectionAnnotation()
             .addCompanionObject()
         properties.addToClass(
-            classBuilder,
-            ClassSettings(ClassSettings.PolymorphyType.NONE, extensions.hasJsonMergePatchExtension)
+            modelName = modelName,
+            classBuilder = classBuilder,
+            classType = ClassSettings(ClassSettings.PolymorphyType.NONE, extensions.hasJsonMergePatchExtension)
         )
         return classBuilder.build()
     }
 
+    private fun polymorphicSuperSubType(
+        modelName: String,
+        properties: Collection<PropertyInfo>,
+        discriminator: Discriminator,
+        superType: SchemaInfo,
+        extensions: Map<String, Any>,
+        allSchemas: List<SchemaInfo>
+    ): TypeSpec = with(FunSpec.constructorBuilder()) {
+        TypeSpec.classBuilder(generatedType(packages.base, modelName))
+            .buildPolymorphicSubType(modelName, properties.filter(PropertyInfo::isInherited), superType, extensions, this)
+            .buildPolymorphicSuperType(modelName, properties.filterNot(PropertyInfo::isInherited), discriminator, extensions, allSchemas, this)
+            .build()
+        }
+
     private fun polymorphicSuperType(
+        modelName: String,
+        properties: Collection<PropertyInfo>,
+        discriminator: Discriminator,
+        extensions: Map<String, Any>,
+        allSchemas: List<SchemaInfo>
+    ): TypeSpec = TypeSpec.classBuilder(generatedType(packages.base, modelName))
+        .buildPolymorphicSuperType(modelName, properties, discriminator, extensions, allSchemas)
+        .build()
+
+    private fun TypeSpec.Builder.buildPolymorphicSuperType(
         modelName: String,
         properties: Collection<PropertyInfo>,
         discriminator: Discriminator,
         extensions: Map<String, Any>,
         allSchemas: List<SchemaInfo>,
-    ): TypeSpec {
-        val classBuilder = TypeSpec.classBuilder(generatedType(packages.base, modelName))
-            .addModifiers(KModifier.SEALED)
+        constructorBuilder: FunSpec.Builder = FunSpec.constructorBuilder()
+    ): TypeSpec.Builder {
+        this.addModifiers(KModifier.SEALED)
             .addAnnotation(basePolymorphicType(discriminator.propertyName))
+            .modifiers.remove(KModifier.DATA)
 
         val subTypes = allSchemas
             .filter { model ->
                 model.schema.allOfSchemas.any { allOfRef ->
-                    allOfRef.name?.toModelClassName() == modelName && allOfRef.discriminator == discriminator
+                    allOfRef.name?.toModelClassName() == modelName &&
+                            (allOfRef.discriminator == discriminator ||
+                             allOfRef.allOfSchemas.any { it.discriminator == discriminator })
                 }
             }
-        val mappings = subTypes.flatMap { schemaInfo ->
-            discriminator.mappingKeys(schemaInfo.schema).map {
-                it to toModelType(packages.base, KotlinTypeInfo.from(schemaInfo.schema, schemaInfo.name))
-            }
-        }.toMap()
+
+        val mappings: Map<String, TypeName> = subTypes.map { schemaInfo ->
+            discriminator.getDiscriminatorMappings(schemaInfo)
+        }.toMapList()
+            .firstValueMap()
+
         val maybeEnumDiscriminator = properties
             .firstOrNull { it.name == discriminator.propertyName }?.typeInfo as? KotlinTypeInfo.Enum
 
-        classBuilder.addAnnotation(polymorphicSubTypes(mappings, maybeEnumDiscriminator))
+        this.addAnnotation(polymorphicSubTypes(mappings, maybeEnumDiscriminator))
             .addQuarkusReflectionAnnotation()
             .addMicronautIntrospectedAnnotation()
             .addMicronautReflectionAnnotation()
 
         properties.addToClass(
-            classBuilder,
+            modelName,
+            constructorBuilder,
+            this,
             ClassSettings(ClassSettings.PolymorphyType.SUPER, extensions.hasJsonMergePatchExtension)
         )
 
-        return classBuilder.build()
+        return this
     }
 
     private fun polymorphicSubType(
         modelName: String,
         properties: Collection<PropertyInfo>,
         superType: SchemaInfo,
+        extensions: Map<String, Any>
+    ): TypeSpec = TypeSpec.classBuilder(generatedType(packages.base, modelName))
+        .buildPolymorphicSubType(modelName, properties, superType, extensions).build()
+
+    private fun TypeSpec.Builder.buildPolymorphicSubType(
+        modelName: String,
+        allProperties: Collection<PropertyInfo>,
+        superType: SchemaInfo,
         extensions: Map<String, Any>,
-    ): TypeSpec {
-        val classBuilder = TypeSpec.classBuilder(generatedType(packages.base, modelName))
-            .addSerializableInterface()
+        constructorBuilder: FunSpec.Builder = FunSpec.constructorBuilder()
+    ): TypeSpec.Builder {
+        this.addSerializableInterface()
             .addQuarkusReflectionAnnotation()
             .addMicronautIntrospectedAnnotation()
             .addMicronautReflectionAnnotation()
@@ -401,20 +450,34 @@ class JacksonModelGenerator(
             .superclass(
                 toModelType(packages.base, KotlinTypeInfo.from(superType.schema, superType.name))
             )
+
+        val properties = superType.schema.getDiscriminatorForInLinedObjectUnderAllOf()?.let { discriminator ->
+            allProperties.filterNot {
+                when (it) {
+                    is PropertyInfo.Field -> it.isPolymorphicDiscriminator && it.name != discriminator.propertyName
+                    else -> false
+                }
+            }
+        } ?: allProperties
+
         properties.addToClass(
-            classBuilder,
+            modelName,
+            constructorBuilder,
+            this,
             ClassSettings(ClassSettings.PolymorphyType.SUB, extensions.hasJsonMergePatchExtension)
         )
-        return classBuilder.build()
+        return this
     }
 
     private fun Collection<PropertyInfo>.addToClass(
+        modelName: String,
+        constructorBuilder: FunSpec.Builder = FunSpec.constructorBuilder(),
         classBuilder: TypeSpec.Builder,
-        classType: ClassSettings
+        classType: ClassSettings,
     ): TypeSpec.Builder {
-        val constructorBuilder = FunSpec.constructorBuilder()
         this.forEach {
             it.addToClass(
+                modelName,
                 toModelType(
                     packages.base,
                     it.typeInfo,
@@ -435,6 +498,23 @@ class JacksonModelGenerator(
         return classBuilder.primaryConstructor(constructorBuilder.build())
     }
 
+    private fun Discriminator.getDiscriminatorMappings(schemaInfo: SchemaInfo): Map<String, TypeName> =
+        mappingKeys(schemaInfo.schema)
+            .filter { it.value == schemaInfo.schema.name }
+            .map {
+                it.key to toModelType(packages.base, KotlinTypeInfo.from(schemaInfo.schema, schemaInfo.name))
+            }
+            .toMap()
+
+    private fun <K, V> List<Map<K,V>>.toMapList(): Map<K, List<V>> =
+        asSequence()
+            .flatMap {
+                it.asSequence()
+            }.groupBy({ it.key }, { it.value })
+
+    private fun <K, V> Map<K,List<V>>.firstValueMap(): Map<K, V> =
+            map { it.key to it.value.first() }.toMap()
+
     private fun TypeSpec.Builder.addSerializableInterface(): TypeSpec.Builder {
         if (options.any { it == ModelCodeGenOptionType.JAVA_SERIALIZATION })
             this.addSuperinterface(Serializable::class)
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt
index d9c6b83..9354710 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt
@@ -11,6 +11,7 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedObjectDefinition
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isRequired
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSchemaLess
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSimpleMapDefinition
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.safeName
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.safeType
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.toModelClassName
 import com.cjbooms.fabrikt.util.NormalisedString.camelCase
@@ -44,6 +45,7 @@ sealed class PropertyInfo {
                     it.topLevelProperties(
                         maybeMarkInherited(
                             settings,
+                            enclosingSchema,
                             it
                         ),
                         this
@@ -55,8 +57,15 @@ sealed class PropertyInfo {
             return results.distinctBy { it.oasKey }
         }
 
-        private fun maybeMarkInherited(settings: Settings, it: Schema) =
-            settings.copy(markAsInherited = if (it.isInLinedObjectUnderAllOf() || it.hasNoDiscriminator()) settings.markAsInherited else true)
+        private fun maybeMarkInherited(settings: Settings, enclosingSchema: Schema?, it: Schema): Settings {
+            val isInherited = when {
+                it.safeName() == enclosingSchema?.name -> false
+                it.hasNoDiscriminator() -> settings.markAsInherited
+                it.isInLinedObjectUnderAllOf() && it.hasNoDiscriminator() -> settings.markAsInherited
+                else -> true
+            }
+            return settings.copy(markAsInherited = isInherited)
+        }
 
         private fun Schema.getInLinedProperties(
             settings: Settings,
@@ -135,9 +144,9 @@ sealed class PropertyInfo {
         }
     }
 
-    sealed class DiscriminatorKey(val stringValue: String) {
-        class StringKey(value: String) : DiscriminatorKey(value)
-        class EnumKey(value: String) : DiscriminatorKey(value) {
+    sealed class DiscriminatorKey(val stringValue: String, val modelName: String) {
+        class StringKey(value: String, modelName: String) : DiscriminatorKey(value, modelName)
+        class EnumKey(value: String, modelName: String) : DiscriminatorKey(value, modelName) {
             val enumKey = value.toEnumName()
         }
     }
@@ -148,7 +157,7 @@ sealed class PropertyInfo {
         override val schema: Schema,
         override val isInherited: Boolean,
         val isPolymorphicDiscriminator: Boolean,
-        val maybeDiscriminator: DiscriminatorKey?,
+        val maybeDiscriminator: Map<String, DiscriminatorKey>?,
         val enclosingSchema: Schema? = null
     ) : PropertyInfo() {
         override val typeInfo: KotlinTypeInfo =
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
index 5b6199c..32f50bd 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
@@ -36,7 +36,8 @@ object KaizenParserExtensions {
 
     private const val EXTENSIBLE_ENUM_KEY = "x-extensible-enum"
 
-    fun Schema.isPolymorphicSuperType(): Boolean = discriminator?.propertyName != null
+    fun Schema.isPolymorphicSuperType(): Boolean = discriminator?.propertyName != null ||
+            getDiscriminatorForInLinedObjectUnderAllOf()?.propertyName != null
 
     fun Schema.isInlinedObjectDefinition() =
         isObjectType() && !isSchemaLess() && Overlay.of(this).pathFromRoot.contains("properties")
@@ -113,10 +114,17 @@ object KaizenParserExtensions {
     private fun Schema.getSchemaNameInParent(): String? = Overlay.of(this).pathInParent
 
     fun Schema.isPolymorphicSubType(api: OpenApi3): Boolean =
-        getEnclosingSchema(api)?.let { it.allOfSchemas.any { it.discriminator.propertyName != null } } ?: false
+        getEnclosingSchema(api)?.let { schema ->
+            schema.allOfSchemas.any { it.isPolymorphicSuperType() }
+        } ?: false
 
     fun Schema.getSuperType(api: OpenApi3): Schema? =
-        getEnclosingSchema(api)?.let { it.allOfSchemas.firstOrNull { it.discriminator.propertyName != null } }
+        getEnclosingSchema(api)?.let { schema ->
+            schema.allOfSchemas.firstOrNull { it.isPolymorphicSuperType() }
+        }
+
+    fun Schema.getDiscriminatorForInLinedObjectUnderAllOf(): Discriminator? =
+        this.allOfSchemas.firstOrNull { it.isInLinedObjectUnderAllOf() }?.discriminator
 
     private fun Schema.getEnclosingSchema(api: OpenApi3): Schema? =
         api.schemas.values.firstOrNull { it.name == safeName() }
@@ -136,19 +144,19 @@ object KaizenParserExtensions {
     fun Schema.getKeyIfSingleDiscriminatorValue(
         prop: Map.Entry<String, Schema>,
         enclosingSchema: Schema
-    ): PropertyInfo.DiscriminatorKey? =
-        if (isDiscriminatorProperty(prop) && discriminator.mappingKeys(enclosingSchema).size == 1) {
-            discriminator.mappingKeys(enclosingSchema).first().let {
-                if (prop.value.isEnumDefinition()) PropertyInfo.DiscriminatorKey.EnumKey(it)
-                else PropertyInfo.DiscriminatorKey.StringKey(it)
-            }
+    ): Map<String, PropertyInfo.DiscriminatorKey>? =
+        if (isDiscriminatorProperty(prop) && discriminator.mappingKeys(enclosingSchema).isNotEmpty()) {
+            discriminator.mappingKeys(enclosingSchema).map {
+                if (prop.value.isEnumDefinition()) it.key to PropertyInfo.DiscriminatorKey.EnumKey(it.key, it.value)
+                else it.key to PropertyInfo.DiscriminatorKey.StringKey(it.key, it.value)
+            }.toMap()
         } else null
 
-    fun Discriminator.mappingKeys(enclosingSchema: Schema): List<String> {
-        val keys = mappings?.entries?.filter {
-            it.value.toString().contains(enclosingSchema.name)
-        }?.map { it.key }
-        return if (keys.isNullOrEmpty()) listOf(enclosingSchema.safeName().toModelClassName()) else keys
+    fun Discriminator.mappingKeys(enclosingSchema: Schema): Map<String, String> {
+        val discriminatorMappings = mappings?.map { it.key to it.value.split("/").last() }?.toMap()
+        return if (discriminatorMappings.isNullOrEmpty()) {
+            mapOf(enclosingSchema.name to enclosingSchema.safeName().toModelClassName())
+        } else discriminatorMappings
     }
 
     fun Schema.isInLinedObjectUnderAllOf(): Boolean =


Test patch:
diff --git a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
index 7b14a21..2b35b23 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
@@ -43,6 +43,7 @@ class ModelGeneratorTest {
         "oneOfPolymorphicModels",
         "optionalVsRequired",
         "polymorphicModels",
+        "nestedPolymorphicModels",
         "requiredReadOnly",
         "validationAnnotations",
         "wildCardTypes",
diff --git a/src/test/resources/examples/nestedPolymorphicModels/api.yaml b/src/test/resources/examples/nestedPolymorphicModels/api.yaml
new file mode 100644
index 0000000..cc3b623
--- /dev/null
+++ b/src/test/resources/examples/nestedPolymorphicModels/api.yaml
@@ -0,0 +1,157 @@
+openapi: 3.0.0
+paths: {}
+info:
+  title: ""
+  version: ""
+components:
+  schemas:
+    RootDiscriminator:
+      type: string
+      enum:
+        - firstLevelChild
+
+    FirstLevelDiscriminator:
+      type: string
+      enum:
+        - secondLevelChild1
+        - secondLevelChild2
+
+    SecondLevelDiscriminator:
+      type: string
+      enum:
+        - thirdLevelChild1
+        - thirdLevelChild2
+
+    RootType:
+      type: object
+      required:
+        - rootField1
+      discriminator:
+        propertyName: rootDiscriminator
+        mapping:
+          firstLevelChild: '#/components/schemas/FirstLevelChild'
+      properties:
+        rootDiscriminator:
+          $ref: "#/components/schemas/RootDiscriminator"
+        rootField1:
+          type: string
+        rootField2:
+          type: boolean
+
+    FirstLevelChild:
+      allOf:
+        - $ref: '#/components/schemas/RootType'
+        - type: object
+          required:
+            - firstLevelField1
+          discriminator:
+            propertyName: firstLevelDiscriminator
+            mapping:
+              secondLevelChild1: '#/components/schemas/SecondLevelChild1'
+              secondLevelChild2: '#/components/schemas/SecondLevelChild2'
+          properties:
+            firstLevelDiscriminator:
+              $ref: '#/components/schemas/FirstLevelDiscriminator'
+            firstLevelField1:
+              type: string
+            firstLevelField2:
+              type: integer
+
+    SecondLevelChild1:
+      allOf:
+        - $ref: '#/components/schemas/FirstLevelChild'
+        - type: object
+          required:
+            - metadata
+          discriminator:
+            propertyName: secondLevelDiscriminator
+            mapping:
+              thirdLevelChild1: '#/components/schemas/ThirdLevelChild11'
+              thirdLevelChild2: '#/components/schemas/ThirdLevelChild12'
+          properties:
+            secondLevelDiscriminator:
+              $ref: '#/components/schemas/SecondLevelDiscriminator'
+            metadata:
+              $ref: '#/components/schemas/SecondLevelMetadata'
+
+    SecondLevelChild2:
+      allOf:
+        - $ref: '#/components/schemas/FirstLevelChild'
+        - type: object
+          required:
+            - metadata
+          discriminator:
+            propertyName: secondLevelDiscriminator
+            mapping:
+              thirdLevelChild1: '#/components/schemas/ThirdLevelChild21'
+              thirdLevelChild2: '#/components/schemas/ThirdLevelChild22'
+          properties:
+            secondLevelDiscriminator:
+              $ref: '#/components/schemas/SecondLevelDiscriminator'
+            metadata:
+              $ref: '#/components/schemas/SecondLevelMetadata'
+
+    SecondLevelMetadata:
+      type: object
+      required:
+        - obj
+      properties:
+        obj:
+          $ref: "#/components/schemas/CommonObject"
+
+    ThirdLevelChild11:
+      allOf:
+        - $ref: '#/components/schemas/SecondLevelChild1'
+        - type: object
+          required:
+            - creationDate
+          properties:
+            creationDate:
+              type: integer
+              description: timestamp
+
+    ThirdLevelChild12:
+      allOf:
+        - $ref: '#/components/schemas/SecondLevelChild1'
+        - type: object
+          required:
+            - isDeleted
+          properties:
+            isDeleted:
+              type: boolean
+
+    ThirdLevelChild21:
+      allOf:
+        - $ref: '#/components/schemas/SecondLevelChild2'
+        - type: object
+          required:
+            - creationDate
+          properties:
+            creationDate:
+              type: integer
+              description: timestamp
+
+    ThirdLevelChild22:
+      allOf:
+        - $ref: '#/components/schemas/SecondLevelChild2'
+        - type: object
+          required:
+            - isDeleted
+          properties:
+            isDeleted:
+              type: boolean
+
+    CommonObject:
+      type: object
+      required:
+        - filed1
+        - field2
+      properties:
+        filed1:
+          type: string
+          required: true
+          nullable: false
+        field2:
+          type: string
+          required: true
+          nullable: false
\ No newline at end of file
diff --git a/src/test/resources/examples/nestedPolymorphicModels/models/Models.kt b/src/test/resources/examples/nestedPolymorphicModels/models/Models.kt
new file mode 100644
index 0000000..4329349
--- /dev/null
+++ b/src/test/resources/examples/nestedPolymorphicModels/models/Models.kt
@@ -0,0 +1,337 @@
+package examples.nestedPolymorphicModels.models
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.annotation.JsonValue
+import javax.validation.Valid
+import javax.validation.constraints.NotNull
+import kotlin.Boolean
+import kotlin.Int
+import kotlin.String
+import kotlin.collections.Map
+
+data class CommonObject(
+    @param:JsonProperty("filed1")
+    @get:JsonProperty("filed1")
+    @get:NotNull
+    val filed1: String,
+    @param:JsonProperty("field2")
+    @get:JsonProperty("field2")
+    @get:NotNull
+    val field2: String
+)
+
+@JsonTypeInfo(
+    use = JsonTypeInfo.Id.NAME,
+    include = JsonTypeInfo.As.EXISTING_PROPERTY,
+    property = "firstLevelDiscriminator",
+    visible = true
+)
+@JsonSubTypes(
+    JsonSubTypes.Type(
+        value = SecondLevelChild1::class,
+        name =
+        "secondLevelChild1"
+    ),
+    JsonSubTypes.Type(
+        value = SecondLevelChild2::class,
+        name =
+        "secondLevelChild2"
+    )
+)
+sealed class FirstLevelChild(
+    @param:JsonProperty("rootField1")
+    @get:JsonProperty("rootField1")
+    @get:NotNull
+    override val rootField1: String,
+    @param:JsonProperty("rootField2")
+    @get:JsonProperty("rootField2")
+    override val rootField2: Boolean? = null,
+    open val firstLevelField1: String,
+    open val firstLevelField2: Int? = null
+) : RootType(rootField1, rootField2) {
+    @get:JsonProperty("rootDiscriminator")
+    @get:NotNull
+    override val rootDiscriminator: RootDiscriminator = RootDiscriminator.FIRST_LEVEL_CHILD
+
+    abstract val firstLevelDiscriminator: FirstLevelDiscriminator
+}
+
+enum class FirstLevelDiscriminator(
+    @JsonValue
+    val value: String
+) {
+    SECOND_LEVEL_CHILD1("secondLevelChild1"),
+
+    SECOND_LEVEL_CHILD2("secondLevelChild2");
+
+    companion object {
+        private val mapping: Map<String, FirstLevelDiscriminator> =
+            values().associateBy(FirstLevelDiscriminator::value)
+
+        fun fromValue(value: String): FirstLevelDiscriminator? = mapping[value]
+    }
+}
+
+enum class RootDiscriminator(
+    @JsonValue
+    val value: String
+) {
+    FIRST_LEVEL_CHILD("firstLevelChild");
+
+    companion object {
+        private val mapping: Map<String, RootDiscriminator> =
+            values().associateBy(RootDiscriminator::value)
+
+        fun fromValue(value: String): RootDiscriminator? = mapping[value]
+    }
+}
+
+@JsonTypeInfo(
+    use = JsonTypeInfo.Id.NAME,
+    include = JsonTypeInfo.As.EXISTING_PROPERTY,
+    property = "rootDiscriminator",
+    visible = true
+)
+@JsonSubTypes(JsonSubTypes.Type(value = FirstLevelChild::class, name = "firstLevelChild"))
+sealed class RootType(
+    open val rootField1: String,
+    open val rootField2: Boolean? = null
+) {
+    abstract val rootDiscriminator: RootDiscriminator
+}
+
+@JsonTypeInfo(
+    use = JsonTypeInfo.Id.NAME,
+    include = JsonTypeInfo.As.EXISTING_PROPERTY,
+    property = "secondLevelDiscriminator",
+    visible = true
+)
+@JsonSubTypes(
+    JsonSubTypes.Type(
+        value = ThirdLevelChild11::class,
+        name =
+        "thirdLevelChild1"
+    ),
+    JsonSubTypes.Type(
+        value = ThirdLevelChild12::class,
+        name =
+        "thirdLevelChild2"
+    )
+)
+sealed class SecondLevelChild1(
+    @param:JsonProperty("rootField1")
+    @get:JsonProperty("rootField1")
+    @get:NotNull
+    override val rootField1: String,
+    @param:JsonProperty("rootField2")
+    @get:JsonProperty("rootField2")
+    override val rootField2: Boolean? = null,
+    @param:JsonProperty("firstLevelField1")
+    @get:JsonProperty("firstLevelField1")
+    @get:NotNull
+    override val firstLevelField1: String,
+    @param:JsonProperty("firstLevelField2")
+    @get:JsonProperty("firstLevelField2")
+    override val firstLevelField2: Int? = null,
+    open val metadata: SecondLevelMetadata
+) : FirstLevelChild(rootField1, rootField2, firstLevelField1, firstLevelField2) {
+    @get:JsonProperty("firstLevelDiscriminator")
+    @get:NotNull
+    override val firstLevelDiscriminator: FirstLevelDiscriminator =
+        FirstLevelDiscriminator.SECOND_LEVEL_CHILD1
+
+    abstract val secondLevelDiscriminator: SecondLevelDiscriminator
+}
+
+@JsonTypeInfo(
+    use = JsonTypeInfo.Id.NAME,
+    include = JsonTypeInfo.As.EXISTING_PROPERTY,
+    property = "secondLevelDiscriminator",
+    visible = true
+)
+@JsonSubTypes(
+    JsonSubTypes.Type(
+        value = ThirdLevelChild21::class,
+        name =
+        "thirdLevelChild1"
+    ),
+    JsonSubTypes.Type(
+        value = ThirdLevelChild22::class,
+        name =
+        "thirdLevelChild2"
+    )
+)
+sealed class SecondLevelChild2(
+    @param:JsonProperty("rootField1")
+    @get:JsonProperty("rootField1")
+    @get:NotNull
+    override val rootField1: String,
+    @param:JsonProperty("rootField2")
+    @get:JsonProperty("rootField2")
+    override val rootField2: Boolean? = null,
+    @param:JsonProperty("firstLevelField1")
+    @get:JsonProperty("firstLevelField1")
+    @get:NotNull
+    override val firstLevelField1: String,
+    @param:JsonProperty("firstLevelField2")
+    @get:JsonProperty("firstLevelField2")
+    override val firstLevelField2: Int? = null,
+    open val metadata: SecondLevelMetadata
+) : FirstLevelChild(rootField1, rootField2, firstLevelField1, firstLevelField2) {
+    @get:JsonProperty("firstLevelDiscriminator")
+    @get:NotNull
+    override val firstLevelDiscriminator: FirstLevelDiscriminator =
+        FirstLevelDiscriminator.SECOND_LEVEL_CHILD2
+
+    abstract val secondLevelDiscriminator: SecondLevelDiscriminator
+}
+
+enum class SecondLevelDiscriminator(
+    @JsonValue
+    val value: String
+) {
+    THIRD_LEVEL_CHILD1("thirdLevelChild1"),
+
+    THIRD_LEVEL_CHILD2("thirdLevelChild2");
+
+    companion object {
+        private val mapping: Map<String, SecondLevelDiscriminator> =
+            values().associateBy(SecondLevelDiscriminator::value)
+
+        fun fromValue(value: String): SecondLevelDiscriminator? = mapping[value]
+    }
+}
+
+data class SecondLevelMetadata(
+    @param:JsonProperty("obj")
+    @get:JsonProperty("obj")
+    @get:NotNull
+    @get:Valid
+    val obj: CommonObject
+)
+
+data class ThirdLevelChild11(
+    @param:JsonProperty("rootField1")
+    @get:JsonProperty("rootField1")
+    @get:NotNull
+    override val rootField1: String,
+    @param:JsonProperty("rootField2")
+    @get:JsonProperty("rootField2")
+    override val rootField2: Boolean? = null,
+    @param:JsonProperty("firstLevelField1")
+    @get:JsonProperty("firstLevelField1")
+    @get:NotNull
+    override val firstLevelField1: String,
+    @param:JsonProperty("firstLevelField2")
+    @get:JsonProperty("firstLevelField2")
+    override val firstLevelField2: Int? = null,
+    @param:JsonProperty("metadata")
+    @get:JsonProperty("metadata")
+    @get:NotNull
+    @get:Valid
+    override val metadata: SecondLevelMetadata,
+    @param:JsonProperty("creationDate")
+    @get:JsonProperty("creationDate")
+    @get:NotNull
+    val creationDate: Int
+) : SecondLevelChild1(rootField1, rootField2, firstLevelField1, firstLevelField2, metadata) {
+    @get:JsonProperty("secondLevelDiscriminator")
+    @get:NotNull
+    override val secondLevelDiscriminator: SecondLevelDiscriminator =
+        SecondLevelDiscriminator.THIRD_LEVEL_CHILD1
+}
+
+data class ThirdLevelChild12(
+    @param:JsonProperty("rootField1")
+    @get:JsonProperty("rootField1")
+    @get:NotNull
+    override val rootField1: String,
+    @param:JsonProperty("rootField2")
+    @get:JsonProperty("rootField2")
+    override val rootField2: Boolean? = null,
+    @param:JsonProperty("firstLevelField1")
+    @get:JsonProperty("firstLevelField1")
+    @get:NotNull
+    override val firstLevelField1: String,
+    @param:JsonProperty("firstLevelField2")
+    @get:JsonProperty("firstLevelField2")
+    override val firstLevelField2: Int? = null,
+    @param:JsonProperty("metadata")
+    @get:JsonProperty("metadata")
+    @get:NotNull
+    @get:Valid
+    override val metadata: SecondLevelMetadata,
+    @param:JsonProperty("isDeleted")
+    @get:JsonProperty("isDeleted")
+    @get:NotNull
+    val isDeleted: Boolean
+) : SecondLevelChild1(rootField1, rootField2, firstLevelField1, firstLevelField2, metadata) {
+    @get:JsonProperty("secondLevelDiscriminator")
+    @get:NotNull
+    override val secondLevelDiscriminator: SecondLevelDiscriminator =
+        SecondLevelDiscriminator.THIRD_LEVEL_CHILD2
+}
+
+data class ThirdLevelChild21(
+    @param:JsonProperty("rootField1")
+    @get:JsonProperty("rootField1")
+    @get:NotNull
+    override val rootField1: String,
+    @param:JsonProperty("rootField2")
+    @get:JsonProperty("rootField2")
+    override val rootField2: Boolean? = null,
+    @param:JsonProperty("firstLevelField1")
+    @get:JsonProperty("firstLevelField1")
+    @get:NotNull
+    override val firstLevelField1: String,
+    @param:JsonProperty("firstLevelField2")
+    @get:JsonProperty("firstLevelField2")
+    override val firstLevelField2: Int? = null,
+    @param:JsonProperty("metadata")
+    @get:JsonProperty("metadata")
+    @get:NotNull
+    @get:Valid
+    override val metadata: SecondLevelMetadata,
+    @param:JsonProperty("creationDate")
+    @get:JsonProperty("creationDate")
+    @get:NotNull
+    val creationDate: Int
+) : SecondLevelChild2(rootField1, rootField2, firstLevelField1, firstLevelField2, metadata) {
+    @get:JsonProperty("secondLevelDiscriminator")
+    @get:NotNull
+    override val secondLevelDiscriminator: SecondLevelDiscriminator =
+        SecondLevelDiscriminator.THIRD_LEVEL_CHILD1
+}
+
+data class ThirdLevelChild22(
+    @param:JsonProperty("rootField1")
+    @get:JsonProperty("rootField1")
+    @get:NotNull
+    override val rootField1: String,
+    @param:JsonProperty("rootField2")
+    @get:JsonProperty("rootField2")
+    override val rootField2: Boolean? = null,
+    @param:JsonProperty("firstLevelField1")
+    @get:JsonProperty("firstLevelField1")
+    @get:NotNull
+    override val firstLevelField1: String,
+    @param:JsonProperty("firstLevelField2")
+    @get:JsonProperty("firstLevelField2")
+    override val firstLevelField2: Int? = null,
+    @param:JsonProperty("metadata")
+    @get:JsonProperty("metadata")
+    @get:NotNull
+    @get:Valid
+    override val metadata: SecondLevelMetadata,
+    @param:JsonProperty("isDeleted")
+    @get:JsonProperty("isDeleted")
+    @get:NotNull
+    val isDeleted: Boolean
+) : SecondLevelChild2(rootField1, rootField2, firstLevelField1, firstLevelField2, metadata) {
+    @get:JsonProperty("secondLevelDiscriminator")
+    @get:NotNull
+    override val secondLevelDiscriminator: SecondLevelDiscriminator =
+        SecondLevelDiscriminator.THIRD_LEVEL_CHILD2
+}