Repository: OpenAPITools/openapi-generator (2025-08-12T20:59:49Z)

Issue:
[BUG] [kotlin-client][kotlin-spring] Discriminator is serialized twice when defined as property
#### Bug Report Checklist

- [x] Have you provided a full/minimal spec to reproduce the issue?
- [x] Have you validated the input using an OpenAPI validator?
- [x] Have you [tested with the latest master](https://github.com/OpenAPITools/openapi-generator/wiki/FAQ#how-to-test-with-the-latest-master-of-openapi-generator) to confirm the issue still exists?
- [x] Have you searched for related issues/PRs?
- [x] What's the actual output vs expected output?
- [ ] [Optional] Sponsorship to speed up the bug fix or feature request ([example](https://github.com/OpenAPITools/openapi-generator/issues/6178))

<!--
Please follow the issue template below for bug reports.
Also please indicate in the issue title which language/library is concerned. Eg:  [BUG][JAVA] Bug generating foo with bar 
-->

##### Description

When the property used as discriminator is specified as a property within the model definitions and jackson is used as serialization library, the discriminator is serialized twice.

##### openapi-generator version

7.14.0

##### OpenAPI declaration file content or url

```yaml
components:
  schemas:
    ShapeType:
      type: string
      enum:
        - TRIANGLE
        - SQUARE
    Shape:
      type: object
      required:
        - type
      properties:
        type:
          $ref: "#/components/schemas/ShapeType"
      discriminator:
        propertyName: type
        mapping:
          TRIANGLE: '#/components/schemas/Triangle'
          SQUARE: '#/components/schemas/Square'
    Triangle:
      allOf:
        - $ref: '#/components/schemas/Shape'
        - type: object
          required:
            - base
            - height
          properties:
            base:
              type: number
            height:
              type: number
    Square:
      allOf:
        - $ref: '#/components/schemas/Shape'
        - type: object
          required:
            - side
          properties:
            side:
              type: number

```

produces

```json
[
    {
        "type": "TRIANGLE",
        "base": 1,
        "height": 1,
        "type": "TRIANGLE"
    },
    {
        "type": "SQUARE",
        "side": 1,
        "type": "SQUARE"
    }
]
```

##### Generation Details

Using gradle plugin 
```kotlin
configOptions.set(
    mapOf(
        "dateLibrary" to "java8",
        "disallowAdditionalPropertiesIfNotPresent" to "false",
        "documentationProvider" to "none",
        "enumPropertyNaming" to "original",
        "exceptionHandler" to "false",
        "hideGenerationTimestamp" to "true",
        "interfaceOnly" to "true",
        "modelMutable" to "true",
        "serializationLibrary" to "jackson",
        "useSpringBoot3" to "true",
        "useTags" to "true",
    )
)
```

##### Steps to reproduce

1. Generate using jackson as serialization library
2. Check the JSON payload.

##### Related issues/PRs
Issue [#9441](https://github.com/OpenAPITools/openapi-generator/issues/9441) / PR [#11691](https://github.com/OpenAPITools/openapi-generator/pull/11691)

Issue [#3796](https://github.com/OpenAPITools/openapi-generator/issues/3796)

##### Suggest a fix

Adopt existing solution from Java / JavaSpring


Discussion:
I created [#21734](https://github.com/OpenAPITools/openapi-generator/pull/21734) as proposed solution



Patch:
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/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"),


Test patch:
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<File> 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");
+    }
 }