Repository: cjbooms/fabrikt
Pull request #125 (2022-03-18 12:58:18)

Add support for disciminated oneOf's
This is something very few code generators even try to implement, but it was fairly easy to add to fabrikt. Currently we use inheritance because that's what openapi-generator supports, but apparently the OpenAPI spec people have the opinion that oneOf should be used for modelling sum types[^1]. This is also the only way to model these that is properly supported by swagger-ui.

The implementation is a bit of a rough sketch, but works for my use-case, which I added as a test case in reduced form. I'm very open for feedback, especially around the differentiation of "normal" oneOf's that are generated as Any type and discriminated ones (oneOfSuperInterface called in the implementation).

Some notes:

* it uses sealed *interfaces* in order to not conflict with any existing inheritance, which are a kotlin 1.5+ feature, so maybe this should be behind a feature flag?
* it assumes that any given type will only be part of a single oneOf type. OAI doesn't have this limitation and since the generated interfaces don't have any properties or methods it could work just fine, but I haven't tried.
* there is some duplication that I could get rid of, but I defer that until I know this is something that has a chance of getting merged
* consistency between oneOf members and the mappings is checked. I'm not sure this is necessary, but seems like the right thing to do.
* the current implementation checks for the propertyName within the member fields, but I don't think that's necessary. At least I can imagine that in many cases the explicit field in the members isn't actually necessary or useful. wdyt?

[^1]: The [OAI page](https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/) explicitly states this: "The discriminator is used with anyOf or oneOf keywords *only*."

cjbooms: This is cool, I like the use of Sealed interface. 

I assume you took a look at the support we already have for oneOf using allOf to get the same functionality?
https://github.com/cjbooms/fabrikt/blob/master/src/test/resources/examples/oneOfPolymorphicModels/api.yaml

Using allOf with oneOf, we can guarantee that the discriminator property required for the `oneOf` is present, but it does require some annoying duplication in the API spec.

I'm definitely willing to accept this, but perhaps we should put it behind a feature flag initially. I know from experience that the code generation around allOf anyOf oneOf combinations can be quite brittle, so safer behind a flag first.

I'll give this a proper detailed review next week. But kudos on the work, it will be a very nice addition.


pschichtel: > I assume you took a look at the support we already have for oneOf using allOf to get the same functionality?
> https://github.com/cjbooms/fabrikt/blob/master/src/test/resources/examples/oneOfPolymorphicModels/api.yaml

I did have a look at it, but I didn't manage to get my example to generate anything other than `Any`.

> Using allOf with oneOf, we can guarantee that the discriminator property required for the `oneOf` is present, but it does require some annoying duplication in the API spec.

Yep, that's what I mean with my last note: If the spec doesn't actually require the discriminator property, we could leave it out and avoid the duplication. [This validator](https://apitools.dev/swagger-parser/online/) accepts it. The [OAI spec page](https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/) ("Discriminator" section) is not explicit about this whether the objects need to define the property, but they also don't have any examples where it's not the case.

That would leave us with:

```yaml
    State:
      oneOf:
        - $ref: '#/components/schemas/StateA'
        - $ref: '#/components/schemas/StateB'
      discriminator:
        propertyName: status
        mapping:
          a: '#/components/schemas/StateA'
          b: '#/components/schemas/StateB'
    StateA:
      type: object
    StateB:
      type: object
```

But I guess even if it isn't necessary from the OAI perspective, it is probably a good idea to include it for documentation purposes and other less powerful generators. Otherwise we would have objects that require a field that is not obviously documented.

Including the discriminator property into StateA and StateB currently is also bad in terms of safety:

`data class StateA(val status: Status)` could be instantiated as `StateA(Status.B)` which would compile, but it makes no sense. I think jackson might still do right thing, but then we still have a useless field.

I think trying to filter it out of properties of the member objects might be doable.
pschichtel: > I think trying to filter it out of properties of the member objects might be doable.

@cjbooms what do you think about leaving the discriminator field out of the constructor, but generating an `@transient` field with the value? could be useful for receivers of the object to get the value without first having to pattern match the object.
cjbooms: > what do you think about leaving the discriminator field out of the constructors

When we do polymorphism with allOf we do omit the discriminator property from the constructor, but only when there is a one-to-one discriminator-to-class mapping. See 
https://github.com/cjbooms/fabrikt/blob/master/src/test/resources/examples/enumPolymorphicDiscriminator/api.yaml
```
      discriminator:
        propertyName: some_enum
        mapping:
          obj_one_only: '#/components/schemas/ConcreteImplOne'
          obj_two_first: '#/components/schemas/ConcreteImplTwo'
          obj_two_second: '#/components/schemas/ConcreteImplTwo'
          obj_three: '#/components/schemas/ConcreteImplThree'
```
 ```kotlin
 class ConcreteImplThree() : PolymorphicEnumDiscriminator() {
    @get:JsonProperty("some_enum")
    @get:NotNull
    override val someEnum: EnumDiscriminator = EnumDiscriminator.OBJ_THREE
} 
```
When there is a many-to-one mapping, we have to allow it in the constructor:
```kotlin
data class ConcreteImplTwo(
    @param:JsonProperty("some_enum")
    @get:JsonProperty("some_enum")
    @get:NotNull
    override val someEnum: EnumDiscriminator,
    @param:JsonProperty("some_prop")
    @get:JsonProperty("some_prop")
    val someProp: String? = null
) : PolymorphicEnumDiscriminator()
```

> but generating an @transient field with the value? could be useful for receivers of the object to get the value without first having to pattern match the object.

I don't fully understand this suggestion. I would need to see a worked example to understand if Jackson could still marshal and unmarshal json payloads


Regarding this example
```
State:
  oneOf:
    - $ref: '#/components/schemas/StateA'
    - $ref: '#/components/schemas/StateB'
  discriminator:
    propertyName: status
    mapping:
      a: '#/components/schemas/StateA'
      b: '#/components/schemas/StateB'
StateA:
  type: object
StateB:
  type: object
```

How would Jackson know which class to choose when the json does not contain a `status` property? Also, what would the generated classes for State, StateA, and StateB look like in this case ? 
pschichtel: > When there is a many-to-one mapping, we have to allow it in the constructor:

I didn't even consider many-to-one mappings...

> I would need to see a worked example

```kotlin
@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME,
  include = JsonTypeInfo.As.EXISTING_PROPERTY,
  property = "type",
  visible = true
)
@JsonSubTypes(
  JsonSubTypes.Type(value = Type.A::class, name = "a"),
  JsonSubTypes.Type(value = Type.B::class, name = "b"),
)
sealed interface Type {
    @get:transient
    val type: String

    data class A(val a: String?) : Type {
        override val type = "a"
    }
    data class B(val b: String?) : Type {
        override val type = "b"
    }
}
```

for a spec like:

```yaml
components:
  schemas:
    Type:
      oneOf:
        - $ref: '#/components/schemas/A'
        - $ref: '#/components/schemas/B'
      mappings:
        propertyName: type
        mappings:
          a: '#/components/schemas/A'
          b: '#/components/schemas/B'
    A:
      type: object
      properties:
        type:
          type: string
        a:
          type: string
    B:
      type: object
      properties:
        type:
          type: string
        b:
          type: string
```

Jackson generally ignores `@transient` fields/getters when serializing. Since the JsonTypeInfo is configured, it should still emit a `type` field for the sub type info. When deserializing it also doesn't care for transient fields, but it reads `type` anyway for the type info.

You are right, this is not possible for cases with many-to-one mappings. 


cjbooms: Let's see about getting this work isolated under a generation flag `ModelCodeGenOptionType` that defaults to off initially. Something like:
```
--http-model-opts SEALED_INTERFACES_FOR_ONE_OF
```

That will allow us to iterate on the oneOf generation capabilities, without risk of breaking the allOf polymorphism that is working well out in the wild


pschichtel: fyi I have to get some other things done, but I'll get back to this soon-ish.
pschichtel: sorry for seemingly leaving this to rot, had a shift of priorities, but I will eventually get back to this
cjbooms: > sorry for seemingly leaving this to rot, I had a shift of priorities, but I will eventually get back to this

No problem at all. It'll be a nice feature once ready, but no one is urgently waiting for it. 
pschichtel: Sorry for the long silence. The project that I worked on, that used fabikt, came to an end and priorities shifted quickly. In the mean time I changed course regarding code generation from OAI specs, in part due to the lack of support from the specs (JSON Schema and OAI).

Before wrapping up the project I noticed problems with my last commit here, which I had reverted to get the project done. This was probably the same issue you noticed [here](https://github.com/cjbooms/fabrikt/pull/125#pullrequestreview-968421983).
You could either merge this without the last commit (this worked well), take over from here or close it. Either way, after rebasing it one last time, I'd prefer to spent my time else where.
cjbooms: @pschichtel this is some great work, kudos for gaining such a deep knowledge of both the OAS schema and fabrikt code base!  If there was someone looking for this enhancement I would merge and cut a new release today.

However, I am not sure if we should add this extra complexity to fabrikt without justification, the model generation class is already very complicated.

@averabaq do you have any thoughts here?
averabaq: This is a great addition, but I'd just rather like to resolve #153 first before merging this. I know about a few adopters waiting for nested polymorphic models support, and this would slow down its release. If there's no urgency I'd rather wait for #153 to get released first
pschichtel: @averabaq  @cjbooms I squashed and rebased the MR, compiles and passes tests.
cjbooms: Nice work @pschichtel. I was hoping this wouldn't be too much work after @averabaq change.
I'm open to merging this and cutting a new release

cjbooms: 👍 
averabaq: 👍 

Patch:
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
index 232464e..7a9aefe 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/cli/CodeGenOptions.kt
@@ -33,7 +33,9 @@ enum class ModelCodeGenOptionType(val description: String) {
     QUARKUS_REFLECTION("This option adds @RegisterForReflection to the generated models. Requires dependency \"'io.quarkus:quarkus-core:+\""),
     MICRONAUT_INTROSPECTION("This option adds @Introspected to the generated models. Requires dependency \"'io.micronaut:micronaut-core:+\""),
     MICRONAUT_REFLECTION("This option adds @ReflectiveAccess to the generated models. Requires dependency \"'io.micronaut:micronaut-core:+\""),
-    INCLUDE_COMPANION_OBJECT("This option adds a companion object to the generated models.");
+    INCLUDE_COMPANION_OBJECT("This option adds a companion object to the generated models."),
+    SEALED_INTERFACES_FOR_ONE_OF("This option enables the generation of interfaces for discriminated oneOf types"),
+    ;
 
     override fun toString() = "`${super.toString()}` - $description"
 }
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 56ad9f5..712e0e8 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -1,6 +1,7 @@
 package com.cjbooms.fabrikt.generators.model
 
 import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType
+import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType.SEALED_INTERFACES_FOR_ONE_OF
 import com.cjbooms.fabrikt.configurations.Packages
 import com.cjbooms.fabrikt.generators.ClassSettings
 import com.cjbooms.fabrikt.generators.GeneratorUtils.toClassName
@@ -33,6 +34,7 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedEnumDefinition
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedObjectDefinition
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedTypedAdditionalProperties
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isOneOfPolymorphicTypes
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.isOneOfSuperInterface
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isPolymorphicSubType
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isPolymorphicSuperType
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSimpleType
@@ -179,6 +181,13 @@ class JacksonModelGenerator(
         val modelName = schemaInfo.name.toModelClassName()
         val schemaName = schemaInfo.name
         return when {
+            schemaInfo.schema.isOneOfSuperInterface() && SEALED_INTERFACES_FOR_ONE_OF in options -> oneOfSuperInterface(
+                modelName,
+                schemaInfo.schema.discriminator,
+                allSchemas,
+                schemaInfo.schema.oneOfSchemas,
+                findOneOfSuperInterface(allSchemas, schemaInfo, options),
+            )
             schemaInfo.schema.isPolymorphicSuperType() && schemaInfo.schema.isPolymorphicSubType(api) ->
                 polymorphicSuperSubType(
                     modelName,
@@ -187,6 +196,7 @@ class JacksonModelGenerator(
                     checkNotNull(schemaInfo.schema.getDiscriminatorForInLinedObjectUnderAllOf()),
                     schemaInfo.schema.getSuperType(api)!!.let { SchemaInfo(it.name, it) },
                     schemaInfo.schema.extensions,
+                    findOneOfSuperInterface(allSchemas, schemaInfo, options),
                     allSchemas,
                 )
 
@@ -196,6 +206,7 @@ class JacksonModelGenerator(
                 properties,
                 schemaInfo.schema.discriminator,
                 schemaInfo.schema.extensions,
+                findOneOfSuperInterface(allSchemas, schemaInfo, options),
                 allSchemas,
             )
 
@@ -205,13 +216,44 @@ class JacksonModelGenerator(
                 properties,
                 schemaInfo.schema.getSuperType(api)!!.let { SchemaInfo(it.name, it) },
                 schemaInfo.schema.extensions,
+                findOneOfSuperInterface(allSchemas, schemaInfo, options),
             )
 
             schemaInfo.typeInfo is KotlinTypeInfo.Enum -> buildEnumClass(schemaInfo.typeInfo)
-            else -> standardDataClass(modelName, schemaName, properties, schemaInfo.schema.extensions)
+            else -> standardDataClass(modelName, schemaName, properties, schemaInfo.schema.extensions, findOneOfSuperInterface(allSchemas, schemaInfo, options))
         }
     }
 
+    private fun findOneOfSuperInterface(
+        allSchemas: List<SchemaInfo>,
+        schema: SchemaInfo,
+        options: Set<ModelCodeGenOptionType>,
+    ): Set<SchemaInfo> {
+        if (SEALED_INTERFACES_FOR_ONE_OF !in options) {
+            return emptySet()
+        }
+        return allSchemas
+            .filter { it.schema.discriminator != null && it.schema.oneOfSchemas.isNotEmpty() }
+            .mapNotNull { info ->
+                info.schema.discriminator.mappings
+                    .toList()
+                    .find { (_, ref) ->
+                        ref.endsWith("/${schema.name}")
+                    }
+                    ?.let { (key, _) ->
+                        Pair(key!!, info)
+                    }
+            }
+            .map { (_, parent) ->
+                val field = parent.schema.discriminator.propertyName!!
+                if (!schema.schema.properties.containsKey(field)) {
+                    throw IllegalArgumentException("schema $schema did not have discriminator property")
+                }
+                parent
+            }
+            .toSet()
+    }
+
     private fun buildInLinedModels(
         topLevelProperties: Collection<PropertyInfo>,
         enclosingSchema: Schema,
@@ -230,6 +272,7 @@ class JacksonModelGenerator(
                         it.name,
                         props,
                         it.schema.extensions,
+                        oneOfInterfaces = emptySet(),
                     )
                     val inlinedModels = buildInLinedModels(props, enclosingSchema, apiDocUrl)
                     inlinedModels + currentModel
@@ -247,6 +290,7 @@ class JacksonModelGenerator(
                                 schemaName = it.name,
                                 properties = it.schema.topLevelProperties(HTTP_SETTINGS, enclosingSchema),
                                 extensions = it.schema.extensions,
+                                oneOfInterfaces = emptySet(),
                             ),
                         )
                     } else {
@@ -274,6 +318,7 @@ class JacksonModelGenerator(
                                         schemaName = it.name,
                                         properties = props,
                                         extensions = it.schema.extensions,
+                                        oneOfInterfaces = emptySet(),
                                     )
                                 }
 
@@ -377,6 +422,7 @@ class JacksonModelGenerator(
                 schemaName = schema.safeName(),
                 properties = mapField.schema.additionalPropertiesSchema.topLevelProperties(HTTP_SETTINGS),
                 extensions = mapField.schema.extensions,
+                oneOfInterfaces = emptySet(),
             )
         } else {
             null
@@ -387,19 +433,41 @@ class JacksonModelGenerator(
         schemaName: String,
         properties: Collection<PropertyInfo>,
         extensions: Map<String, Any>,
+        oneOfInterfaces: Set<SchemaInfo>,
     ): TypeSpec {
-        val classBuilder = TypeSpec.classBuilder(generatedType(packages.base, modelName))
+        val filteredProperties = if (oneOfInterfaces.size == 1) {
+            val oneOfInterface = oneOfInterfaces.first()
+            val discriminatorProp = oneOfInterface.schema.discriminator?.propertyName
+            val mappingCount = oneOfInterface.schema.discriminator?.mappings?.values?.count { it.endsWith("/$modelName") }
+            if (discriminatorProp != null && mappingCount == 1) {
+                properties.filterNot { it.name == discriminatorProp }
+            } else properties
+        } else properties
+
+        val name = generatedType(packages.base, modelName)
+        val generateObject = properties.isNotEmpty() && filteredProperties.isEmpty()
+        val builder =
+            if (generateObject) TypeSpec.objectBuilder(name)
+            else TypeSpec.classBuilder(name)
+        val classBuilder = builder
             .addSerializableInterface()
             .addQuarkusReflectionAnnotation()
             .addMicronautIntrospectedAnnotation()
             .addMicronautReflectionAnnotation()
             .addCompanionObject()
-        properties.addToClass(
+        for (oneOfInterface in oneOfInterfaces) {
+            classBuilder
+                .addSuperinterface(generatedType(packages.base, oneOfInterface.schema.toModelClassName()))
+        }
+
+        if (!generateObject) {
+            filteredProperties.addToClass(
             modelName = modelName,
             schemaName = schemaName,
             classBuilder = classBuilder,
             classType = ClassSettings(ClassSettings.PolymorphyType.NONE, extensions.hasJsonMergePatchExtension),
         )
+        }
         return classBuilder.build()
     }
 
@@ -410,6 +478,7 @@ class JacksonModelGenerator(
         discriminator: Discriminator,
         superType: SchemaInfo,
         extensions: Map<String, Any>,
+        oneOfSuperInterfaces: Set<SchemaInfo>,
         allSchemas: List<SchemaInfo>,
     ): TypeSpec = with(FunSpec.constructorBuilder()) {
         TypeSpec.classBuilder(generatedType(packages.base, modelName))
@@ -419,6 +488,7 @@ class JacksonModelGenerator(
                 properties.filter(PropertyInfo::isInherited),
                 superType,
                 extensions,
+                oneOfSuperInterfaces,
                 this,
             )
             .buildPolymorphicSuperType(
@@ -427,21 +497,62 @@ class JacksonModelGenerator(
                 properties.filterNot(PropertyInfo::isInherited),
                 discriminator,
                 extensions,
+                oneOfSuperInterfaces,
                 allSchemas,
                 this,
             )
             .build()
     }
 
+    private fun oneOfSuperInterface(
+        modelName: String,
+        discriminator: Discriminator,
+        allSchemas: List<SchemaInfo>,
+        members: List<Schema>,
+        oneOfSuperInterfaces: Set<SchemaInfo>,
+    ): TypeSpec {
+        val interfaceBuilder = TypeSpec.interfaceBuilder(generatedType(packages.base, modelName))
+            .addModifiers(KModifier.SEALED)
+            .addAnnotation(basePolymorphicType(discriminator.propertyName))
+
+        for (oneOfSuperInterface in oneOfSuperInterfaces) {
+            interfaceBuilder.addSuperinterface(generatedType(packages.base, oneOfSuperInterface.name))
+        }
+
+        val membersAndMappingsConsistent = members.all { member ->
+            discriminator.mappings.any { (_, ref) -> ref.endsWith("/${member.name}") }
+        }
+
+        if (!membersAndMappingsConsistent) {
+            throw IllegalArgumentException("members and mappings are not consistent for oneOf super interface $modelName!")
+        }
+
+        val mappings = discriminator.mappings
+            .mapValues { (_, value) ->
+                allSchemas.find { value.endsWith("/${it.name}") }!!
+            }
+            .mapValues { (_, value) ->
+                toModelType(packages.base, KotlinTypeInfo.from(value.schema, value.name))
+            }
+
+        interfaceBuilder.addAnnotation(polymorphicSubTypes(mappings, enumDiscriminator = null))
+            .addQuarkusReflectionAnnotation()
+            .addMicronautIntrospectedAnnotation()
+            .addMicronautReflectionAnnotation()
+
+        return interfaceBuilder.build()
+    }
+
     private fun polymorphicSuperType(
         modelName: String,
         schemaName: String,
         properties: Collection<PropertyInfo>,
         discriminator: Discriminator,
         extensions: Map<String, Any>,
+        oneOfSuperInterfaces: Set<SchemaInfo>,
         allSchemas: List<SchemaInfo>,
     ): TypeSpec = TypeSpec.classBuilder(generatedType(packages.base, modelName))
-        .buildPolymorphicSuperType(modelName, schemaName, properties, discriminator, extensions, allSchemas)
+        .buildPolymorphicSuperType(modelName, schemaName, properties, discriminator, extensions, oneOfSuperInterfaces, allSchemas)
         .build()
 
     private fun TypeSpec.Builder.buildPolymorphicSuperType(
@@ -450,6 +561,7 @@ class JacksonModelGenerator(
         properties: Collection<PropertyInfo>,
         discriminator: Discriminator,
         extensions: Map<String, Any>,
+        oneOfSuperInterfaces: Set<SchemaInfo>,
         allSchemas: List<SchemaInfo>,
         constructorBuilder: FunSpec.Builder = FunSpec.constructorBuilder(),
     ): TypeSpec.Builder {
@@ -457,6 +569,10 @@ class JacksonModelGenerator(
             .addAnnotation(basePolymorphicType(discriminator.propertyName))
             .modifiers.remove(KModifier.DATA)
 
+        for (oneOfSuperInterface in oneOfSuperInterfaces) {
+            this.addSuperinterface(generatedType(packages.base, oneOfSuperInterface.name))
+        }
+
         val subTypes = allSchemas
             .filter { model ->
                 model.schema.allOfSchemas.any { allOfRef ->
@@ -498,8 +614,9 @@ class JacksonModelGenerator(
         properties: Collection<PropertyInfo>,
         superType: SchemaInfo,
         extensions: Map<String, Any>,
+        oneOfSuperInterfaces: Set<SchemaInfo>,
     ): TypeSpec = TypeSpec.classBuilder(generatedType(packages.base, modelName))
-        .buildPolymorphicSubType(modelName, schemaName, properties, superType, extensions).build()
+        .buildPolymorphicSubType(modelName, schemaName, properties, superType, extensions, oneOfSuperInterfaces).build()
 
     private fun TypeSpec.Builder.buildPolymorphicSubType(
         modelName: String,
@@ -507,6 +624,7 @@ class JacksonModelGenerator(
         allProperties: Collection<PropertyInfo>,
         superType: SchemaInfo,
         extensions: Map<String, Any>,
+        oneOfSuperInterfaces: Set<SchemaInfo>,
         constructorBuilder: FunSpec.Builder = FunSpec.constructorBuilder(),
     ): TypeSpec.Builder {
         this.addSerializableInterface()
@@ -518,6 +636,10 @@ class JacksonModelGenerator(
                 toModelType(packages.base, KotlinTypeInfo.from(superType.schema, superType.name)),
             )
 
+        for (oneOfSuperInterface in oneOfSuperInterfaces) {
+            this.addSuperinterface(generatedType(packages.base, oneOfSuperInterface.name))
+        }
+
         val properties = superType.schema.getDiscriminatorForInLinedObjectUnderAllOf()?.let { discriminator ->
             allProperties.filterNot {
                 when (it) {
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
index 95128aa..7227951 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
@@ -6,6 +6,7 @@ import com.cjbooms.fabrikt.model.OasType.Companion.toOasType
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.getEnumValues
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedTypedAdditionalProperties
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isNotDefined
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.isOneOfSuperInterface
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.toMapValueClassName
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.toModelClassName
 import com.cjbooms.fabrikt.util.NormalisedString.toModelClassName
@@ -92,7 +93,9 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
                         from(schema.additionalPropertiesSchema, "", enclosingName)
                     )
                 OasType.Any -> AnyType
-                OasType.OneOfAny -> AnyType
+                OasType.OneOfAny ->
+                    if (schema.isOneOfSuperInterface()) Object(schema.toModelClassName(enclosingName.toModelClassName()))
+                    else AnyType
             }
 
         private fun getOverridableDateTimeType(): KotlinTypeInfo {
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
index ac864a3..4924c24 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
@@ -102,7 +102,7 @@ object KaizenParserExtensions {
         "additionalProperties" && properties?.isEmpty() != true && !isSimpleType()
 
     fun Schema.isSimpleType(): Boolean =
-        (simpleTypes.contains(type) && !isEnumDefinition()) || isSimpleMapDefinition() || isSimpleOneOfAnyDefinition()
+        !isOneOfSuperInterface() && ((simpleTypes.contains(type) && !isEnumDefinition()) || isSimpleMapDefinition() || isSimpleOneOfAnyDefinition())
 
     private fun Schema.isObjectType() = OasType.Object.type == type
 
@@ -210,6 +210,9 @@ object KaizenParserExtensions {
     fun Schema.isOneOfPolymorphicTypes() =
         this.oneOfSchemas?.firstOrNull()?.allOfSchemas?.firstOrNull() != null
 
+    fun Schema.isOneOfSuperInterface() =
+        discriminator != null && discriminator.propertyName != null && oneOfSchemas.isNotEmpty()
+
     fun OpenApi3.basePath(): String =
         servers
             .firstOrNull()


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 780adea..553fbd7 100644
--- a/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
+++ b/src/test/kotlin/com/cjbooms/fabrikt/generators/ModelGeneratorTest.kt
@@ -51,6 +51,8 @@ class ModelGeneratorTest {
         "responsesSchema",
         "webhook",
         "instantDateTime",
+        "singleAllOf",
+        "discriminatedOneOf",
     )
 
     @BeforeEach
@@ -79,6 +81,7 @@ class ModelGeneratorTest {
         val models = JacksonModelGenerator(
             Packages(basePackage),
             sourceApi,
+            setOf(ModelCodeGenOptionType.SEALED_INTERFACES_FOR_ONE_OF),
         ).generate().toSingleFile()
 
         assertThat(models).isEqualTo(expectedModels)
diff --git a/src/test/resources/examples/discriminatedOneOf/api.yaml b/src/test/resources/examples/discriminatedOneOf/api.yaml
new file mode 100644
index 0000000..ab2e0a2
--- /dev/null
+++ b/src/test/resources/examples/discriminatedOneOf/api.yaml
@@ -0,0 +1,40 @@
+openapi: 3.0.0
+info:
+paths:
+components:
+  schemas:
+    SomeObj:
+      type: object
+      required:
+        - state
+      properties:
+        state:
+          $ref: '#/components/schemas/State'
+    State:
+      oneOf:
+        - $ref: '#/components/schemas/StateA'
+        - $ref: '#/components/schemas/StateB'
+      discriminator:
+        propertyName: status
+        mapping:
+          a: '#/components/schemas/StateA'
+          b: '#/components/schemas/StateB'
+    Status:
+      type: string
+      enum:
+        - a
+        - b
+    StateA:
+      type: object
+      required:
+        - status
+      properties:
+        status:
+          $ref: '#/components/schemas/Status'
+    StateB:
+      type: object
+      required:
+        - status
+      properties:
+        status:
+          $ref: '#/components/schemas/Status'
diff --git a/src/test/resources/examples/discriminatedOneOf/models/Models.kt b/src/test/resources/examples/discriminatedOneOf/models/Models.kt
new file mode 100644
index 0000000..d1e4fb1
--- /dev/null
+++ b/src/test/resources/examples/discriminatedOneOf/models/Models.kt
@@ -0,0 +1,53 @@
+package examples.discriminatedOneOf.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.String
+import kotlin.collections.Map
+
+data class SomeObj(
+    @param:JsonProperty("state")
+    @get:JsonProperty("state")
+    @get:NotNull
+    @get:Valid
+    val state: State
+)
+
+@JsonTypeInfo(
+    use = JsonTypeInfo.Id.NAME,
+    include = JsonTypeInfo.As.EXISTING_PROPERTY,
+    property = "status",
+    visible = true
+)
+@JsonSubTypes(
+    JsonSubTypes.Type(value = StateA::class, name = "a"),
+    JsonSubTypes.Type(
+        value =
+        StateB::class,
+        name = "b"
+    )
+)
+sealed interface State
+
+object StateA : State
+
+object StateB : State
+
+enum class Status(
+    @JsonValue
+    val value: String
+) {
+    A("a"),
+
+    B("b");
+
+    companion object {
+        private val mapping: Map<String, Status> = values().associateBy(Status::value)
+
+        fun fromValue(value: String): Status? = mapping[value]
+    }
+}