diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/data_class.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/data_class.mustache index 9837a1272385..b62f84b050c7 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/data_class.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/data_class.mustache @@ -43,6 +43,12 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder {{/enumUnknownDefaultCase}} {{#hasEnums}} +{{^enumUnknownDefaultCase}} +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +{{/enumUnknownDefaultCase}} {{/hasEnums}} {{#discriminator}} import kotlinx.serialization.ExperimentalSerializationApi @@ -144,7 +150,7 @@ import {{packageName}}.infrastructure.ITransformForStorage */ {{^multiplatform}} {{#kotlinx_serialization}} - {{#serializableModel}}@KSerializable{{/serializableModel}}{{^serializableModel}}@Serializable{{#enumUnknownDefaultCase}}(with = {{{nameInPascalCase}}}Serializer::class){{/enumUnknownDefaultCase}}{{/serializableModel}} + {{#serializableModel}}@KSerializable{{/serializableModel}}{{^serializableModel}}@Serializable{{#enumUnknownDefaultCase}}(with = {{{nameInPascalCase}}}Serializer::class){{/enumUnknownDefaultCase}}{{/serializableModel}}{{^enumUnknownDefaultCase}}{{^isString}}{{^isNumber}}{{^isContainer}}(with = {{{nameInPascalCase}}}Serializer::class){{/isContainer}}{{/isNumber}}{{/isString}}{{/enumUnknownDefaultCase}} {{/kotlinx_serialization}} {{#moshi}} @JsonClass(generateAdapter = false) @@ -201,6 +207,30 @@ import {{packageName}}.infrastructure.ITransformForStorage } } {{/enumUnknownDefaultCase}} + {{^multiplatform}} + {{^enumUnknownDefaultCase}} + {{^isString}} + {{^isNumber}} + {{^isContainer}} + + internal object {{nameInPascalCase}}Serializer : KSerializer<{{nameInPascalCase}}> { + override val descriptor = {{dataType}}.serializer().descriptor + + override fun deserialize(decoder: Decoder): {{nameInPascalCase}} { + val value = decoder.decodeSerializableValue({{dataType}}.serializer()) + return {{nameInPascalCase}}.entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Unknown enum value: $value") + } + + override fun serialize(encoder: Encoder, value: {{nameInPascalCase}}) { + encoder.encodeSerializableValue({{dataType}}.serializer(), value.value) + } + } + {{/isContainer}} + {{/isNumber}} + {{/isString}} + {{/enumUnknownDefaultCase}} + {{/multiplatform}} {{/kotlinx_serialization}} {{/isEnum}} {{/vars}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java index 1a90a73cfbad..f08003d35c79 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenModelTest.java @@ -1582,4 +1582,68 @@ public void testEnumUnknownDefaultCaseIsRegisteredAsCliOption() { Assert.assertEquals(option.getDefault(), "false"); Assert.assertEquals(option.getEnum().keySet(), Set.of("true", "false")); } + + private Path generateKotlinxInlineNumericEnumModels(boolean serializableModel) throws IOException { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName(KOTLIN_GENERATOR) + .setInputSpec("src/test/resources/3_0/kotlin/issue25459-kotlinx-inline-numeric-enum.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")) + .addAdditionalProperty(CodegenConstants.SERIALIZATION_LIBRARY, "kotlinx_serialization") + .addAdditionalProperty(CodegenConstants.SERIALIZABLE_MODEL, serializableModel); + + DefaultGenerator generator = new DefaultGenerator(); + generator.opts(configurator.toClientOptInput()).generate(); + + return Paths.get(output.getAbsolutePath(), "src/main/kotlin/org/openapitools/client/models"); + } + + @Test + public void testKotlinxSerializationInlineNumericEnumUsesValueSerializer() throws IOException { + Path models = generateKotlinxInlineNumericEnumModels(false); + Path recordingKt = models.resolve("Recording.kt"); + + // A plain @Serializable enum is written by serial name, so Plays would be the JSON string "1" + // instead of the number 1. The value serializer reads and writes the Int itself. + TestUtils.assertFileContains(recordingKt, + "@Serializable(with = PlaysSerializer::class)", + "enum class Plays(val value: kotlin.Int)", + "internal object PlaysSerializer : KSerializer", + "decoder.decodeSerializableValue(kotlin.Int.serializer())", + "encoder.encodeSerializableValue(kotlin.Int.serializer(), value.value)", + "import kotlinx.serialization.KSerializer", + "import kotlinx.serialization.builtins.serializer", + "import kotlinx.serialization.encoding.Decoder", + "import kotlinx.serialization.encoding.Encoder"); + // String enums stay on their @SerialName values. + TestUtils.assertFileNotContains(recordingKt, "ModeSerializer"); + // Models without enums must not pick up the serializer imports. + TestUtils.assertFileNotContains(models.resolve("Label.kt"), "KSerializer"); + } + + @Test + public void testKotlinxSerializationInlineBigDecimalEnumKeepsPlainSerializable() throws IOException { + Path recordingKt = generateKotlinxInlineNumericEnumModels(false).resolve("Recording.kt"); + + // type: number maps to java.math.BigDecimal, which has no serializer() to build a value serializer from. + TestUtils.assertFileContains(recordingKt, + "@Serializable enum class Ratio(val value: java.math.BigDecimal)"); + TestUtils.assertFileNotContains(recordingKt, "RatioSerializer", "java.math.BigDecimal.serializer()"); + // type: number with format double maps to kotlin.Double and keeps the value serializer. + TestUtils.assertFileContains(recordingKt, + "@Serializable(with = WeightSerializer::class)", + "encoder.encodeSerializableValue(kotlin.Double.serializer(), value.value)"); + } + + @Test + public void testKotlinxSerializationInlineNumericEnumUsesValueSerializerForSerializableModel() throws IOException { + // serializableModel imports kotlinx's Serializable under the alias KSerializable. + Path recordingKt = generateKotlinxInlineNumericEnumModels(true).resolve("Recording.kt"); + + TestUtils.assertFileContains(recordingKt, + "@KSerializable(with = PlaysSerializer::class)", + "internal object PlaysSerializer : KSerializer"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/issue25459-kotlinx-inline-numeric-enum.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/issue25459-kotlinx-inline-numeric-enum.yaml new file mode 100644 index 000000000000..1ca44ef55ea1 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/issue25459-kotlinx-inline-numeric-enum.yaml @@ -0,0 +1,46 @@ +openapi: 3.0.3 +info: + title: 'Issue 25459 kotlinx_serialization inline numeric enum' + version: latest +paths: + '/recording': + get: + operationId: getRecording + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Recording' +components: + schemas: + Recording: + type: object + required: [plays, mode, speed] + properties: + plays: + type: integer + enum: [1, 2] + mode: + type: string + enum: [fast, slow] + ratio: + type: number + enum: [0.5, 1.5] + weight: + type: number + format: double + enum: [0.25, 0.75] + speed: + $ref: '#/components/schemas/Speed' + label: + $ref: '#/components/schemas/Label' + Speed: + type: integer + enum: [1, 2] + Label: + type: object + properties: + text: + type: string diff --git a/samples/client/petstore/kotlin-jvm-ktor-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Order.kt b/samples/client/petstore/kotlin-jvm-ktor-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Order.kt index 51661832e6b3..5cc1f25afceb 100644 --- a/samples/client/petstore/kotlin-jvm-ktor-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Order.kt +++ b/samples/client/petstore/kotlin-jvm-ktor-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Order.kt @@ -27,6 +27,10 @@ package org.openapitools.client.models import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName import kotlinx.serialization.Contextual +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder /** * An order for a pets from the pet store diff --git a/samples/client/petstore/kotlin-jvm-ktor-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Pet.kt b/samples/client/petstore/kotlin-jvm-ktor-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Pet.kt index f0028c2c8bf6..47bc0ca3b0e8 100644 --- a/samples/client/petstore/kotlin-jvm-ktor-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Pet.kt +++ b/samples/client/petstore/kotlin-jvm-ktor-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Pet.kt @@ -29,6 +29,10 @@ import org.openapitools.client.models.Tag import kotlinx.serialization.Serializable import kotlinx.serialization.SerialName import kotlinx.serialization.Contextual +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder /** * A pet for sale in the pet store diff --git a/samples/client/petstore/kotlin-retrofit2-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Order.kt b/samples/client/petstore/kotlin-retrofit2-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Order.kt index 380ff0fd1167..000f293ca8a7 100644 --- a/samples/client/petstore/kotlin-retrofit2-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Order.kt +++ b/samples/client/petstore/kotlin-retrofit2-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Order.kt @@ -27,6 +27,10 @@ package org.openapitools.client.models import kotlinx.serialization.Serializable as KSerializable import kotlinx.serialization.SerialName import kotlinx.serialization.Contextual +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import java.io.Serializable /** diff --git a/samples/client/petstore/kotlin-retrofit2-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Pet.kt b/samples/client/petstore/kotlin-retrofit2-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Pet.kt index b7f0c745c253..bc52f70399fd 100644 --- a/samples/client/petstore/kotlin-retrofit2-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Pet.kt +++ b/samples/client/petstore/kotlin-retrofit2-kotlinx_serialization/src/main/kotlin/org/openapitools/client/models/Pet.kt @@ -29,6 +29,10 @@ import org.openapitools.client.models.Tag import kotlinx.serialization.Serializable as KSerializable import kotlinx.serialization.SerialName import kotlinx.serialization.Contextual +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import java.io.Serializable /**