Repository: cjbooms/fabrikt (kotlin)

Issue:
AnyType for oneOf
This is a followup for #106.

openapi-generator considers `foo` to be of type "any"  in the following example using `oneOf`:

```yml
openapi: 3.0.0
components:
  schemas:
    Foo:
      type: object
      properties:
        foo:
          oneOf:
            - type: string
            - type: object
              properties:
                a:
                  type: string
                b:
                  type: string

```

It looks like right now fabrikt just uses the first schema here:

```kotlin
data class Foo(
  @param:JsonProperty("foo")
  @get:JsonProperty("foo")
  val foo: String? = null
)
```

Pull request description:
A oneOf that can not be resolved to a polymorphic definition should be treated as an Any.

Closes #110

Also fixes the duplicate key handling by replacing a `results.toSet()` with `results.distinctBy { it.oasKey }`

Patch:
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 048ab1ff..8eeac6d3 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/JacksonModelGenerator.kt
@@ -230,6 +230,7 @@ class JacksonModelGenerator(
                             else -> emptySet()
                         }
                     }
+                is PropertyInfo.OneOfAny -> emptySet()
             }
     }
 
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
index cda32ea2..d2699c17 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinTypeInfo.kt
@@ -83,6 +83,7 @@ sealed class KotlinTypeInfo(val modelKClass: KClass<*>, val generatedModelClassN
                         from(schema.additionalPropertiesSchema, "", enclosingName)
                     )
                 OasType.Any -> AnyType
+                OasType.OneOfAny -> AnyType
             }
     }
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/OasType.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/OasType.kt
index 0f747ca0..a93a8bb7 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/OasType.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/OasType.kt
@@ -1,7 +1,8 @@
 package com.cjbooms.fabrikt.model
 
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isEnumDefinition
-import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlineableMapDefinition
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSimpleMapDefinition
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSimpleOneOfAnyDefinition
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isMapTypeAdditionalProperties
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSchemaLess
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isTypedAdditionalProperties
@@ -19,6 +20,7 @@ sealed class OasType(
     val specialization: Specialization = Specialization.NONE
 ) {
     object Any : OasType(null)
+    object OneOfAny : OasType("object", specialization = Specialization.ONE_OF_ANY)
     object Boolean : OasType("boolean")
     object Date : OasType("string", "date")
     object DateTime : OasType("string", "date-time")
@@ -69,7 +71,8 @@ sealed class OasType(
                 isUuidDefinition() -> Specialization.UUID
                 isEnumDefinition() -> Specialization.ENUM
                 isMapTypeAdditionalProperties(oasKey) -> Specialization.TYPED_MAP_ADDITIONAL_PROPERTIES
-                isInlineableMapDefinition() -> Specialization.MAP
+                isSimpleMapDefinition() -> Specialization.MAP
+                isSimpleOneOfAnyDefinition() -> Specialization.ONE_OF_ANY
                 isTypedAdditionalProperties(oasKey) -> Specialization.TYPED_OBJECT_ADDITIONAL_PROPERTIES
                 isUntypedAdditionalProperties(oasKey) -> Specialization.UNTYPED_OBJECT_ADDITIONAL_PROPERTIES
                 isUnknownAdditionalProperties(oasKey) -> Specialization.UNKNOWN_ADDITIONAL_PROPERTIES
@@ -87,6 +90,7 @@ sealed class OasType(
         UNTYPED_OBJECT_ADDITIONAL_PROPERTIES,
         UNTYPED_OBJECT,
         UUID,
-        NONE
+        NONE,
+        ONE_OF_ANY
     }
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt
index 939c6f94..0126771d 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/model/PropertyInfo.kt
@@ -6,7 +6,7 @@ import com.cjbooms.fabrikt.util.KaizenParserExtensions.hasNoDiscriminator
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isDiscriminatorProperty
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isEnumDefinition
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInLinedObjectUnderAllOf
-import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlineableMapDefinition
+import com.cjbooms.fabrikt.util.KaizenParserExtensions.isSimpleMapDefinition
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedArrayDefinition
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedEnumDefinition
 import com.cjbooms.fabrikt.util.KaizenParserExtensions.isInlinedObjectDefinition
@@ -40,7 +40,7 @@ sealed class PropertyInfo {
         val HTTP_SETTINGS = Settings()
 
         fun Schema.topLevelProperties(settings: Settings, enclosingSchema: Schema? = null): Collection<PropertyInfo> {
-            val results = emptyList<PropertyInfo>() +
+            val results = mutableListOf<PropertyInfo>() +
                 allOfSchemas.flatMap {
                     it.topLevelProperties(
                         maybeMarkInherited(
@@ -50,10 +50,10 @@ sealed class PropertyInfo {
                         this
                     )
                 } +
-                oneOfSchemas.flatMap { it.topLevelProperties(settings.copy(markAllOptional = true), this) } +
+                (if (oneOfSchemas.isEmpty()) emptyList() else listOf(OneOfAny(oneOfSchemas.first()))) +
                 anyOfSchemas.flatMap { it.topLevelProperties(settings.copy(markAllOptional = true), this) } +
                 getInLinedProperties(settings, enclosingSchema)
-            return results.toSet()
+            return results.distinctBy { it.oasKey }
         }
 
         private fun maybeMarkInherited(settings: Settings, it: Schema) =
@@ -77,7 +77,7 @@ sealed class PropertyInfo {
                             else null
                         )
                     OasType.Object.type ->
-                        if (property.value.isInlineableMapDefinition() || property.value.isSchemaLess())
+                        if (property.value.isSimpleMapDefinition() || property.value.isSchemaLess())
                             MapField(
                                 isRequired = isRequired(
                                     property,
@@ -162,17 +162,7 @@ sealed class PropertyInfo {
         val maximum: Number? = schema.safeField(Schema::getMaximum)
         val exclusiveMaximum: Boolean? = schema.safeField(Schema::getExclusiveMaximum)
 
-        private fun <T> Schema.safeField(getField: Schema.() -> T?): T? =
-            when {
-                this.getField() != null -> this.getField()
-                allOfSchemas?.firstOrNull { it.getField() != null } != null ->
-                    allOfSchemas.first { it.getField() != null }.getField()
-                oneOfSchemas?.firstOrNull { it.getField() != null } != null ->
-                    oneOfSchemas.first { it.getField() != null }.getField()
-                anyOfSchemas?.firstOrNull { it.getField() != null } != null ->
-                    anyOfSchemas.first { it.getField() != null }.getField()
-                else -> null
-            }
+        private fun <T> Schema.safeField(getField: Schema.() -> T?): T? = this.getField()
     }
 
     interface CollectionValidation {
@@ -235,4 +225,11 @@ sealed class PropertyInfo {
         override val typeInfo: KotlinTypeInfo = KotlinTypeInfo.from(schema, "additionalProperties")
         override val isRequired: Boolean = true
     }
+
+    data class OneOfAny(
+        override val schema: Schema,
+    ) : PropertyInfo() {
+        override val oasKey: String = "oneOf"
+        override val typeInfo: KotlinTypeInfo = KotlinTypeInfo.AnyType
+    }
 }
diff --git a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
index 131be31e..a684c715 100644
--- a/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
+++ b/src/main/kotlin/com/cjbooms/fabrikt/util/KaizenParserExtensions.kt
@@ -56,7 +56,13 @@ object KaizenParserExtensions {
 
     fun Schema.isSchemaLess() = isObjectType() && properties?.isEmpty() == true
 
-    fun Schema.isInlineableMapDefinition() = hasAdditionalProperties() && properties?.isEmpty() == true
+    fun Schema.isSimpleMapDefinition() = hasAdditionalProperties() && properties?.isEmpty() == true
+
+    fun Schema.isSimpleOneOfAnyDefinition() = oneOfSchemas?.isNotEmpty() == true &&
+        !isOneOfPolymorphicTypes() &&
+        anyOfSchemas?.isEmpty() == true &&
+        allOfSchemas?.isEmpty() == true &&
+        properties?.isEmpty() == true
 
     fun Schema.isEnumDefinition(): Boolean = this.type == OasType.Text.type && (
         this.hasEnums() || (
@@ -95,7 +101,7 @@ object KaizenParserExtensions {
         "additionalProperties" && properties?.isEmpty() != true && !isSimpleType()
 
     fun Schema.isSimpleType(): Boolean =
-        (simpleTypes.contains(type) && !isEnumDefinition()) || isInlineableMapDefinition()
+        (simpleTypes.contains(type) && !isEnumDefinition()) || isSimpleMapDefinition() || isSimpleOneOfAnyDefinition()
 
     private fun Schema.isObjectType() = OasType.Object.type == type
 


Test patch:
diff --git a/src/test/resources/examples/anyOfOneOfAllOf/api.yaml b/src/test/resources/examples/anyOfOneOfAllOf/api.yaml
index a69ac8d0..047b8206 100644
--- a/src/test/resources/examples/anyOfOneOfAllOf/api.yaml
+++ b/src/test/resources/examples/anyOfOneOfAllOf/api.yaml
@@ -1,6 +1,12 @@
 openapi: 3.0.0
 components:
   schemas:
+    SimpleOneOfs:
+      type: object
+      properties: 
+        oneof_property:
+          $ref: '#/components/schemas/ContainsNestedOneOf'
+
     ComplexParent:
       allOf:
         - $ref: '#/components/schemas/ContainsNestedOneOf'
diff --git a/src/test/resources/examples/anyOfOneOfAllOf/models/Models.kt b/src/test/resources/examples/anyOfOneOfAllOf/models/Models.kt
index bafccb38..6ecb0d9d 100644
--- a/src/test/resources/examples/anyOfOneOfAllOf/models/Models.kt
+++ b/src/test/resources/examples/anyOfOneOfAllOf/models/Models.kt
@@ -6,12 +6,9 @@ import kotlin.Any
 import kotlin.String
 
 data class ComplexParent(
-    @param:JsonProperty("first_nested_one_of_prop")
-    @get:JsonProperty("first_nested_one_of_prop")
-    val firstNestedOneOfProp: String? = null,
-    @param:JsonProperty("more_nested_prop_one")
-    @get:JsonProperty("more_nested_prop_one")
-    val moreNestedPropOne: String? = null,
+    @param:JsonProperty("oneOf")
+    @get:JsonProperty("oneOf")
+    val oneOf: Any? = null,
     @param:JsonProperty("first_nested_any_of_prop")
     @get:JsonProperty("first_nested_any_of_prop")
     val firstNestedAnyOfProp: String? = null,
@@ -38,15 +35,6 @@ data class ContainsNestedAnyOf(
     val secondNestedAnyOfProp: String? = null
 )
 
-data class ContainsNestedOneOf(
-    @param:JsonProperty("first_nested_one_of_prop")
-    @get:JsonProperty("first_nested_one_of_prop")
-    val firstNestedOneOfProp: String? = null,
-    @param:JsonProperty("more_nested_prop_one")
-    @get:JsonProperty("more_nested_prop_one")
-    val moreNestedPropOne: String? = null
-)
-
 data class FirstAnyA(
     @param:JsonProperty("first_nested_any_of_prop")
     @get:JsonProperty("first_nested_any_of_prop")
@@ -88,3 +76,9 @@ data class SecondOneB(
     @get:JsonProperty("forth_property")
     val forthProperty: Any? = null
 )
+
+data class SimpleOneOfs(
+    @param:JsonProperty("oneof_property")
+    @get:JsonProperty("oneof_property")
+    val oneofProperty: Any? = null
+)
diff --git a/src/test/resources/examples/duplicatePropertyHandling/api.yaml b/src/test/resources/examples/duplicatePropertyHandling/api.yaml
index 6d776a5c..fbc92261 100644
--- a/src/test/resources/examples/duplicatePropertyHandling/api.yaml
+++ b/src/test/resources/examples/duplicatePropertyHandling/api.yaml
@@ -3,17 +3,17 @@ components:
   schemas:
     DuplicatesParent:
       allOf:
-        - $ref: '#/components/schemas/ContainsNestedOneOfWithDupes'
+        - $ref: '#/components/schemas/ContainsNestedAnyOfWithDupes'
         - type: object
           properties:
             top_level_duplicate:
               type: number
 
-    ContainsNestedOneOfWithDupes:
-      oneOf:
+    ContainsNestedAnyOfWithDupes:
+      anyOf:
         - $ref: "#/components/schemas/FirstOneD"
         - $ref: "#/components/schemas/FirstOneD"
-        - $ref: "#/components/schemas/FirstOneBDuplicate"
+        - $ref: "#/components/schemas/TheDuplicator"
 
     FirstOneD:
       type: object
diff --git a/src/test/resources/examples/duplicatePropertyHandling/models/Models.kt b/src/test/resources/examples/duplicatePropertyHandling/models/Models.kt
index 63ec3718..350f06bd 100644
--- a/src/test/resources/examples/duplicatePropertyHandling/models/Models.kt
+++ b/src/test/resources/examples/duplicatePropertyHandling/models/Models.kt
@@ -1,13 +1,15 @@
 package examples.duplicatePropertyHandling.models
 
 import com.fasterxml.jackson.annotation.JsonProperty
-import java.math.BigDecimal
 import kotlin.String
 
-data class ContainsNestedOneOfWithDupes(
+data class ContainsNestedAnyOfWithDupes(
     @param:JsonProperty("child_duplicate")
     @get:JsonProperty("child_duplicate")
-    val childDuplicate: String? = null
+    val childDuplicate: String? = null,
+    @param:JsonProperty("top_level_duplicate")
+    @get:JsonProperty("top_level_duplicate")
+    val topLevelDuplicate: String? = null
 )
 
 data class DuplicatesParent(
@@ -16,7 +18,7 @@ data class DuplicatesParent(
     val childDuplicate: String? = null,
     @param:JsonProperty("top_level_duplicate")
     @get:JsonProperty("top_level_duplicate")
-    val topLevelDuplicate: BigDecimal? = null
+    val topLevelDuplicate: String? = null
 )
 
 data class FirstOneD(