From d64621f07072d7797ffa12015f350a5287635396 Mon Sep 17 00:00:00 2001 From: Daniel Genchev Date: Tue, 12 Aug 2025 22:41:16 +0200 Subject: [PATCH 1/2] [kotlin-client][kotlin-spring] Fix duplicate discriminator serialization with Jackson used as serialization library --- .../languages/KotlinSpringServerCodegen.java | 4 +- .../kotlin-client/data_class.mustache | 1 + .../kotlin-client/typeInfoAnnotation.mustache | 4 ++ .../kotlin-spring/typeInfoAnnotation.mustache | 5 +- .../kotlin/KotlinClientCodegenModelTest.java | 58 ++++++++++++++++--- .../kotlin/KotlinSpringServerCodegenTest.java | 41 +++++++++++++ 6 files changed, 103 insertions(+), 10 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java index 14f5faba4946..25e1ff186e2e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinSpringServerCodegen.java @@ -20,7 +20,6 @@ import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Mustache.Lambda; import com.samskivert.mustache.Template; -import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import lombok.Getter; @@ -425,6 +424,7 @@ public void processOpts() { importMapping.put("JsonProperty", "com.fasterxml.jackson.annotation.JsonProperty"); importMapping.put("JsonSubTypes", "com.fasterxml.jackson.annotation.JsonSubTypes"); importMapping.put("JsonTypeInfo", "com.fasterxml.jackson.annotation.JsonTypeInfo"); + importMapping.put("JsonIgnoreProperties", "com.fasterxml.jackson.annotation.JsonIgnoreProperties"); // import JsonCreator if JsonProperty is imported // used later in recursive import in postProcessingModels importMapping.put("com.fasterxml.jackson.annotation.JsonProperty", "com.fasterxml.jackson.annotation.JsonCreator"); @@ -827,7 +827,7 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert } if (model.discriminator != null && additionalProperties.containsKey("jackson")) { - model.imports.addAll(Arrays.asList("JsonSubTypes", "JsonTypeInfo")); + model.imports.addAll(Arrays.asList("JsonSubTypes", "JsonTypeInfo", "JsonIgnoreProperties")); } } 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 8ca3989d9981..f7da96b3c8aa 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 @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue {{/enumUnknownDefaultCase}} import com.fasterxml.jackson.annotation.JsonProperty {{#discriminator}} +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo {{/discriminator}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/typeInfoAnnotation.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/typeInfoAnnotation.mustache index 6b3cb83b0f8e..c35ef3c4be2b 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/typeInfoAnnotation.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/typeInfoAnnotation.mustache @@ -1,3 +1,7 @@ +@JsonIgnoreProperties( + value = ["{{{discriminator.propertyBaseName}}}"], // ignore manually set {{{discriminator.propertyBaseName}}}, it will be automatically generated by Jackson during serialization + allowSetters = true // allows the {{{discriminator.propertyBaseName}}} to be set during deserialization +) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) @JsonSubTypes( {{#discriminator.mappedModels}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-spring/typeInfoAnnotation.mustache b/modules/openapi-generator/src/main/resources/kotlin-spring/typeInfoAnnotation.mustache index 35d740aa79a3..b459faf88aa6 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-spring/typeInfoAnnotation.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-spring/typeInfoAnnotation.mustache @@ -1,5 +1,8 @@ {{#jackson}} - +@JsonIgnoreProperties( + value = ["{{{discriminator.propertyBaseName}}}"], // ignore manually set {{{discriminator.propertyBaseName}}}, it will be automatically generated by Jackson during serialization + allowSetters = true // allows the {{{discriminator.propertyBaseName}}} to be set during deserialization +) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "{{{discriminator.propertyBaseName}}}", visible = true) @JsonSubTypes( {{#discriminator.mappedModels}} 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 023622cdc3a9..3e1ed97b5a32 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 @@ -19,17 +19,15 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.*; -import lombok.Getter; -import org.antlr.v4.runtime.*; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.ParseTreeWalker; import org.openapitools.codegen.*; import org.openapitools.codegen.antlr4.KotlinLexer; import org.openapitools.codegen.antlr4.KotlinParser; -import org.openapitools.codegen.antlr4.KotlinParserBaseListener; import org.openapitools.codegen.config.CodegenConfigurator; import org.openapitools.codegen.languages.KotlinClientCodegen; -import org.openapitools.codegen.languages.KotlinServerCodegen; import org.openapitools.codegen.testutils.ConfigAssert; import org.testng.Assert; import org.testng.annotations.DataProvider; @@ -45,8 +43,6 @@ import java.util.Map; import static org.openapitools.codegen.CodegenConstants.*; -import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.INTERFACE_ONLY; -import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.RETURN_RESPONSE; @SuppressWarnings("static-method") public class KotlinClientCodegenModelTest { @@ -534,7 +530,7 @@ private void givenSchemaObjectPropertyNameContainsDollarSignWhenGenerateThenDoll } @Test(description = "generate polymorphic kotlinx_serialization model") - public void polymorphicKotlinxSerialzation() throws IOException { + public void polymorphicKotlinxSerialization() throws IOException { File output = Files.createTempDirectory("test").toFile(); output.deleteOnExit(); @@ -573,6 +569,54 @@ public void polymorphicKotlinxSerialzation() throws IOException { TestUtils.assertFileContains(birdKt, "@SerialName(value = \"BIRD\")"); } + @Test(description = "generate polymorphic jackson model") + public void polymorphicJacksonSerialization() throws IOException { + File output = Files.createTempDirectory("test").toFile(); +// output.deleteOnExit(); + + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("kotlin") + .setLibrary("jvm-okhttp4") + .setAdditionalProperties(new HashMap<>() {{ + put(CodegenConstants.SERIALIZATION_LIBRARY, "jackson"); + put(CodegenConstants.MODEL_PACKAGE, "xyz.abcdef.model"); + }}) + .setInputSpec("src/test/resources/3_0/kotlin/polymorphism.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + List files = generator.opts(clientOptInput).generate(); + + Assert.assertEquals(files.size(), 28); + + final Path animalKt = Paths.get(output + "/src/main/kotlin/xyz/abcdef/model/Animal.kt"); + // base has extra jackson imports + TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonIgnoreProperties"); + TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonSubTypes"); + TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonTypeInfo"); + // and these are being used + TestUtils.assertFileContains(animalKt, "@JsonIgnoreProperties"); + TestUtils.assertFileContains(animalKt, "@JsonSubTypes"); + TestUtils.assertFileContains(animalKt, "@JsonTypeInfo"); + // base is interface + TestUtils.assertFileContains(animalKt, "interface Animal"); + // base properties are present + TestUtils.assertFileContains(animalKt, "val id"); + TestUtils.assertFileContains(animalKt, "val optionalProperty"); + // base doesn't contain discriminator + TestUtils.assertFileNotContains(animalKt, "val discriminator"); + + final Path birdKt = Paths.get(output + "/src/main/kotlin/xyz/abcdef/model/Bird.kt"); + // derived has serial name set to mapping key + TestUtils.assertFileContains(birdKt, "data class Bird"); + // derived properties are overridden + TestUtils.assertFileContains(birdKt, "override val id"); + TestUtils.assertFileContains(birdKt, "override val optionalProperty"); + // derived doesn't contain disciminator + TestUtils.assertFileNotContains(birdKt, "val discriminator"); + } + private static class ModelNameTest { private final String expectedName; private final String expectedClassName; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinSpringServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinSpringServerCodegenTest.java index c0d34d64b660..86db332541e1 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinSpringServerCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinSpringServerCodegenTest.java @@ -60,4 +60,45 @@ public void gradleWrapperIsGenerated() throws IOException { //Different because file is not a text file assertTrue(Files.exists(gradleWrapperJarCloud)); } + + @Test(description = "generate polymorphic jackson model") + public void polymorphicJacksonSerialization() throws IOException { + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + + KotlinSpringServerCodegen codegen = new KotlinSpringServerCodegen() ; + codegen.setOutputDir(output.getAbsolutePath()); + + new DefaultGenerator().opts( + new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/3_0/kotlin/polymorphism.yaml")) + .config(codegen) + ).generate(); + + final Path animalKt = Paths.get(output + "/src/main/kotlin/org/openapitools/model/Animal.kt"); + // base has extra jackson imports + TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonIgnoreProperties"); + TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonSubTypes"); + TestUtils.assertFileContains(animalKt, "import com.fasterxml.jackson.annotation.JsonTypeInfo"); + // and these are being used + TestUtils.assertFileContains(animalKt, "@JsonIgnoreProperties"); + TestUtils.assertFileContains(animalKt, "@JsonSubTypes"); + TestUtils.assertFileContains(animalKt, "@JsonTypeInfo"); + // base is interface + TestUtils.assertFileContains(animalKt, "interface Animal"); + // base properties are present + TestUtils.assertFileContains(animalKt, "val id"); + TestUtils.assertFileContains(animalKt, "val optionalProperty"); + // base doesn't contain discriminator + TestUtils.assertFileNotContains(animalKt, "val discriminator"); + + final Path birdKt = Paths.get(output + "/src/main/kotlin/org/openapitools/model/Bird.kt"); + // derived has serial name set to mapping key + TestUtils.assertFileContains(birdKt, "data class Bird"); + // derived properties are overridden + TestUtils.assertFileContains(birdKt, "override val id"); + TestUtils.assertFileContains(birdKt, "override val optionalProperty"); + // derived doesn't contain disciminator + TestUtils.assertFileNotContains(birdKt, "val discriminator"); + } } From 456298bfe2e5dc3c8d67803736a6ff1b75d877c9 Mon Sep 17 00:00:00 2001 From: Daniel Genchev Date: Wed, 13 Aug 2025 10:37:00 +0200 Subject: [PATCH 2/2] Update samples --- .../src/main/kotlin/org/openapitools/model/Animal.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/samples/server/petstore/kotlin-springboot-request-cookie/src/main/kotlin/org/openapitools/model/Animal.kt b/samples/server/petstore/kotlin-springboot-request-cookie/src/main/kotlin/org/openapitools/model/Animal.kt index 5f5cafa6291f..71072eada589 100644 --- a/samples/server/petstore/kotlin-springboot-request-cookie/src/main/kotlin/org/openapitools/model/Animal.kt +++ b/samples/server/petstore/kotlin-springboot-request-cookie/src/main/kotlin/org/openapitools/model/Animal.kt @@ -1,6 +1,7 @@ package org.openapitools.model import java.util.Objects +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo @@ -20,7 +21,10 @@ import io.swagger.v3.oas.annotations.media.Schema * @param className * @param color */ - +@JsonIgnoreProperties( + value = ["className"], // ignore manually set className, it will be automatically generated by Jackson during serialization + allowSetters = true // allows the className to be set during deserialization +) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "className", visible = true) @JsonSubTypes( JsonSubTypes.Type(value = Cat::class, name = "CAT"),