Skip to content

Commit 6b32b43

Browse files
Omit discriminator property for Kotlin serialization (#342)
Generating the discriminator property causes a name conflict with Kotlin Serialization's class discriminator. Example: `Sealed class 'subclass' cannot be serialized as base class 'com.example.models.Superclass' because it has property name that conflicts with JSON class discriminator 'type'.`
1 parent 89f6d46 commit 6b32b43

File tree

9 files changed

+74
-21
lines changed

9 files changed

+74
-21
lines changed

end2end-tests/models-kotlinx/openapi/api.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,24 @@ components:
7171
LandlinePhone:
7272
type: object
7373
required:
74+
- type
7475
- number
7576
- area_code
7677
properties:
78+
type:
79+
type: string
7780
number:
7881
type: string
7982
area_code:
8083
type: string
8184
MobilePhone:
8285
type: object
8386
required:
87+
- type
8488
- number
8589
properties:
90+
type:
91+
type: string
8692
number:
8793
type: string
8894
Error:

src/main/kotlin/com/cjbooms/fabrikt/generators/GeneratorUtils.kt

+29
Original file line numberDiff line numberDiff line change
@@ -219,4 +219,33 @@ object GeneratorUtils {
219219
}
220220

221221
private fun isNullable(parameter: Parameter): Boolean = !parameter.isRequired && parameter.schema.default == null
222+
223+
/**
224+
* Converts a TypeSpec to an object by copying over all properties, functions, etc.
225+
*/
226+
fun TypeSpec.toObjectTypeSpec(): TypeSpec {
227+
require(name != null) { "Name must be set to convert to object" }
228+
229+
val objectBuilder = TypeSpec.objectBuilder(name!!)
230+
.addAnnotations(annotations)
231+
.addModifiers(modifiers)
232+
.superclass(superclass)
233+
.addProperties(propertySpecs)
234+
.addFunctions(funSpecs)
235+
.addKdoc(kdoc)
236+
237+
for ((typeName, _) in superinterfaces) {
238+
objectBuilder.addSuperinterface(typeName)
239+
}
240+
241+
if (initializerBlock.isNotEmpty()) {
242+
objectBuilder.addInitializerBlock(initializerBlock)
243+
}
244+
245+
for (nestedType in typeSpecs) {
246+
objectBuilder.addType(nestedType)
247+
}
248+
249+
return objectBuilder.build()
250+
}
222251
}

src/main/kotlin/com/cjbooms/fabrikt/generators/PropertyUtils.kt

+15-11
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,22 @@ object PropertyUtils {
127127
if (isDiscriminatorFieldWithSingleKnownValue(classSettings, schemaName)) {
128128
this as PropertyInfo.Field
129129
if (classSettings.polymorphyType in listOf(ClassSettings.PolymorphyType.SUB, ClassSettings.PolymorphyType.ONE_OF)) {
130-
property.initializer(name)
131-
serializationAnnotations.addParameter(property, oasKey)
132-
val constructorParameter: ParameterSpec.Builder = ParameterSpec.builder(name, wrappedType)
133-
val discriminators = maybeDiscriminator.getDiscriminatorMappings(schemaName)
134-
when (val discriminator = discriminators.first()) {
135-
is PropertyInfo.DiscriminatorKey.EnumKey ->
136-
constructorParameter.defaultValue("%T.%L", wrappedType, discriminator.enumKey)
137-
138-
is PropertyInfo.DiscriminatorKey.StringKey ->
139-
constructorParameter.defaultValue("%S", discriminator.stringValue)
130+
if (!serializationAnnotations.supportsBackingPropertyForDiscriminator) {
131+
return // Skip adding the property to the class
132+
} else {
133+
property.initializer(name)
134+
serializationAnnotations.addParameter(property, oasKey)
135+
val constructorParameter: ParameterSpec.Builder = ParameterSpec.builder(name, wrappedType)
136+
val discriminators = maybeDiscriminator.getDiscriminatorMappings(schemaName)
137+
when (val discriminator = discriminators.first()) {
138+
is PropertyInfo.DiscriminatorKey.EnumKey ->
139+
constructorParameter.defaultValue("%T.%L", wrappedType, discriminator.enumKey)
140+
141+
is PropertyInfo.DiscriminatorKey.StringKey ->
142+
constructorParameter.defaultValue("%S", discriminator.stringValue)
143+
}
144+
constructorBuilder.addParameter(constructorParameter.build())
140145
}
141-
constructorBuilder.addParameter(constructorParameter.build())
142146
}
143147
} else {
144148
property.initializer(name)

src/main/kotlin/com/cjbooms/fabrikt/generators/model/ModelGenerator.kt

+9-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.cjbooms.fabrikt.cli.ModelCodeGenOptionType.SEALED_INTERFACES_FOR_ONE_
66
import com.cjbooms.fabrikt.configurations.Packages
77
import com.cjbooms.fabrikt.generators.ClassSettings
88
import com.cjbooms.fabrikt.generators.GeneratorUtils.toClassName
9+
import com.cjbooms.fabrikt.generators.GeneratorUtils.toObjectTypeSpec
910
import com.cjbooms.fabrikt.generators.MutableSettings
1011
import com.cjbooms.fabrikt.generators.PropertyUtils.addToClass
1112
import com.cjbooms.fabrikt.generators.PropertyUtils.isNullable
@@ -498,7 +499,14 @@ class ModelGenerator(
498499

499500
serializationAnnotations.addClassAnnotation(classBuilder)
500501

501-
return classBuilder.build()
502+
val classTypeSpec = classBuilder.build()
503+
504+
return if (classBuilder.propertySpecs.isNotEmpty()) {
505+
classTypeSpec
506+
} else {
507+
// properties have been filtered out in generation process so return an object instead
508+
classTypeSpec.toObjectTypeSpec()
509+
}
502510
}
503511

504512
private fun polymorphicSuperSubType(

src/main/kotlin/com/cjbooms/fabrikt/model/JacksonAnnotations.kt

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.squareup.kotlinpoet.TypeName
1010
import com.squareup.kotlinpoet.TypeSpec
1111

1212
object JacksonAnnotations : SerializationAnnotations {
13+
override val supportsBackingPropertyForDiscriminator = true
1314
override val supportsAdditionalProperties = true
1415
override fun addIgnore(propertySpecBuilder: PropertySpec.Builder) =
1516
propertySpecBuilder.addAnnotation(JacksonMetadata.ignore)

src/main/kotlin/com/cjbooms/fabrikt/model/KotlinxSerializationAnnotations.kt

+8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import kotlinx.serialization.SerialName
99
import kotlinx.serialization.Serializable
1010

1111
object KotlinxSerializationAnnotations : SerializationAnnotations {
12+
/**
13+
* Polymorphic class discriminators are added as annotations in kotlinx serialization.
14+
* Including them in the class definition causes compilation errors since the property name
15+
* will conflict with the class discriminator name.
16+
*/
17+
override val supportsBackingPropertyForDiscriminator = false
18+
1219
/**
1320
* Supporting "additionalProperties: true" for kotlinx serialization requires additional
1421
* research and work due to Any type in the map (val properties: MutableMap<String, Any?>)
@@ -18,6 +25,7 @@ object KotlinxSerializationAnnotations : SerializationAnnotations {
1825
* See also https://github.com/Kotlin/kotlinx.serialization/issues/1978
1926
*/
2027
override val supportsAdditionalProperties = false
28+
2129
override fun addIgnore(propertySpecBuilder: PropertySpec.Builder) =
2230
propertySpecBuilder // not applicable
2331

src/main/kotlin/com/cjbooms/fabrikt/model/SerializationAnnotations.kt

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import com.squareup.kotlinpoet.TypeName
66
import com.squareup.kotlinpoet.TypeSpec
77

88
sealed interface SerializationAnnotations {
9+
/**
10+
* Whether to include backing property for a polymorphic discriminator
11+
*/
12+
val supportsBackingPropertyForDiscriminator: Boolean
13+
914
/**
1015
* Whether the annotation supports OpenAPI's additional properties
1116
* https://spec.openapis.org/oas/v3.0.0.html#model-with-map-dictionary-properties
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
package examples.discriminatedOneOf.models
22

3-
import javax.validation.constraints.NotNull
43
import kotlinx.serialization.SerialName
54
import kotlinx.serialization.Serializable
65

76
@SerialName("a")
87
@Serializable
9-
public data class StateA(
10-
@SerialName("status")
11-
@get:NotNull
12-
public val status: Status = Status.A,
13-
) : State
8+
public object StateA : State

src/test/resources/examples/discriminatedOneOf/models/kotlinx/StateB.kt

-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,4 @@ public data class StateB(
1010
@SerialName("mode")
1111
@get:NotNull
1212
public val mode: StateBMode,
13-
@SerialName("status")
14-
@get:NotNull
15-
public val status: Status = Status.B,
1613
) : State

0 commit comments

Comments
 (0)