diff --git a/end2end-tests/models-kotlinx/openapi/api.yaml b/end2end-tests/models-kotlinx/openapi/api.yaml index 5b9e8586..912c5e82 100644 --- a/end2end-tests/models-kotlinx/openapi/api.yaml +++ b/end2end-tests/models-kotlinx/openapi/api.yaml @@ -71,9 +71,12 @@ components: LandlinePhone: type: object required: + - type - number - area_code properties: + type: + type: string number: type: string area_code: @@ -81,8 +84,11 @@ components: MobilePhone: type: object required: + - type - number properties: + type: + type: string number: type: string Error: diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt index 876d2007..0557eaa1 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt @@ -219,4 +219,33 @@ object GeneratorUtils { } private fun isNullable(parameter: Parameter): Boolean = !parameter.isRequired && parameter.schema.default == null + + /** + * Converts a TypeSpec to an object by copying over all properties, functions, etc. + */ + fun TypeSpec.toObjectTypeSpec(): TypeSpec { + require(name != null) { "Name must be set to convert to object" } + + val objectBuilder = TypeSpec.objectBuilder(name!!) + .addAnnotations(annotations) + .addModifiers(modifiers) + .superclass(superclass) + .addProperties(propertySpecs) + .addFunctions(funSpecs) + .addKdoc(kdoc) + + for ((typeName, _) in superinterfaces) { + objectBuilder.addSuperinterface(typeName) + } + + if (initializerBlock.isNotEmpty()) { + objectBuilder.addInitializerBlock(initializerBlock) + } + + for (nestedType in typeSpecs) { + objectBuilder.addType(nestedType) + } + + return objectBuilder.build() + } } diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt index 3ef4a4e9..ddc3e9fb 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt @@ -127,18 +127,22 @@ object PropertyUtils { if (isDiscriminatorFieldWithSingleKnownValue(classSettings, schemaName)) { this as PropertyInfo.Field if (classSettings.polymorphyType in listOf(ClassSettings.PolymorphyType.SUB, ClassSettings.PolymorphyType.ONE_OF)) { - property.initializer(name) - serializationAnnotations.addParameter(property, oasKey) - val constructorParameter: ParameterSpec.Builder = ParameterSpec.builder(name, wrappedType) - val discriminators = maybeDiscriminator.getDiscriminatorMappings(schemaName) - when (val discriminator = discriminators.first()) { - is PropertyInfo.DiscriminatorKey.EnumKey -> - constructorParameter.defaultValue("%T.%L", wrappedType, discriminator.enumKey) - - is PropertyInfo.DiscriminatorKey.StringKey -> - constructorParameter.defaultValue("%S", discriminator.stringValue) + if (!serializationAnnotations.supportsBackingPropertyForDiscriminator) { + return // Skip adding the property to the class + } else { + property.initializer(name) + serializationAnnotations.addParameter(property, oasKey) + val constructorParameter: ParameterSpec.Builder = ParameterSpec.builder(name, wrappedType) + val discriminators = maybeDiscriminator.getDiscriminatorMappings(schemaName) + when (val discriminator = discriminators.first()) { + is PropertyInfo.DiscriminatorKey.EnumKey -> + constructorParameter.defaultValue("%T.%L", wrappedType, discriminator.enumKey) + + is PropertyInfo.DiscriminatorKey.StringKey -> + constructorParameter.defaultValue("%S", discriminator.stringValue) + } + constructorBuilder.addParameter(constructorParameter.build()) } - constructorBuilder.addParameter(constructorParameter.build()) } } else { property.initializer(name) diff --git a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/ModelGenerator.kt b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/ModelGenerator.kt index 987ef1f0..c3f1c007 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/generators/model/ModelGenerator.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/generators/model/ModelGenerator.kt @@ -6,6 +6,7 @@ import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType.SEALED_INTERFACES_FOR_ONE_ import com.cjbooms.fabrikt.configurations.Packages import com.cjbooms.fabrikt.generators.ClassSettings import com.cjbooms.fabrikt.generators.GeneratorUtils.toClassName +import com.cjbooms.fabrikt.generators.GeneratorUtils.toObjectTypeSpec import com.cjbooms.fabrikt.generators.MutableSettings import com.cjbooms.fabrikt.generators.PropertyUtils.addToClass import com.cjbooms.fabrikt.generators.PropertyUtils.isNullable @@ -498,7 +499,14 @@ class ModelGenerator( serializationAnnotations.addClassAnnotation(classBuilder) - return classBuilder.build() + val classTypeSpec = classBuilder.build() + + return if (classBuilder.propertySpecs.isNotEmpty()) { + classTypeSpec + } else { + // properties have been filtered out in generation process so return an object instead + classTypeSpec.toObjectTypeSpec() + } } private fun polymorphicSuperSubType( diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt index 126fe958..e3686be2 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt @@ -10,6 +10,7 @@ import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec object JacksonAnnotations : SerializationAnnotations { + override val supportsBackingPropertyForDiscriminator = true override val supportsAdditionalProperties = true override fun addIgnore(propertySpecBuilder: PropertySpec.Builder) = propertySpecBuilder.addAnnotation(JacksonMetadata.ignore) diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt index 46264ca0..a5057183 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt @@ -9,6 +9,13 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable object KotlinxSerializationAnnotations : SerializationAnnotations { + /** + * Polymorphic class discriminators are added as annotations in kotlinx serialization. + * Including them in the class definition causes compilation errors since the property name + * will conflict with the class discriminator name. + */ + override val supportsBackingPropertyForDiscriminator = false + /** * Supporting "additionalProperties: true" for kotlinx serialization requires additional * research and work due to Any type in the map (val properties: MutableMap) @@ -18,6 +25,7 @@ object KotlinxSerializationAnnotations : SerializationAnnotations { * See also https://github.com/Kotlin/kotlinx.serialization/issues/1978 */ override val supportsAdditionalProperties = false + override fun addIgnore(propertySpecBuilder: PropertySpec.Builder) = propertySpecBuilder // not applicable diff --git a/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt b/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt index 57e354f5..fe874ea3 100644 --- a/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt +++ b/src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt @@ -6,6 +6,11 @@ import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec sealed interface SerializationAnnotations { + /** + * Whether to include backing property for a polymorphic discriminator + */ + val supportsBackingPropertyForDiscriminator: Boolean + /** * Whether the annotation supports OpenAPI's additional properties * https://spec.openapis.org/oas/v3.0.0.html#model-with-map-dictionary-properties diff --git a/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateA.kt b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateA.kt index 14d80492..5a137423 100644 --- a/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateA.kt +++ b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateA.kt @@ -1,13 +1,8 @@ package examples.discriminatedOneOf.models -import javax.validation.constraints.NotNull import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @SerialName("a") @Serializable -public data class StateA( - @SerialName("status") - @get:NotNull - public val status: Status = Status.A, -) : State +public object StateA : State diff --git a/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateB.kt b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateB.kt index 8498cfa6..57134695 100644 --- a/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateB.kt +++ b/src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateB.kt @@ -10,7 +10,4 @@ public data class StateB( @SerialName("mode") @get:NotNull public val mode: StateBMode, - @SerialName("status") - @get:NotNull - public val status: Status = Status.B, ) : State