[BUG][KOTLIN-SERVER] typeInfoAnnotation.mustache uses As.PROPERTY → discriminator property serialized twice

Open Beginner friendly
#25,524 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
62/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
java, kotlin, openapi
Domain
api, backend

Research direction

The annotation is rendered by modules/openapi-generator/src/main/resources/kotlin-server/typeInfoAnnotation.mustache, which sets the JsonTypeInfo include mode to As.PROPERTY. Start by generating the spec in the report with kotlin-server and library jaxrs-spec, then serialize a Cat instance with plain Jackson to confirm the duplicated kind key. Done means the output contains kind once, with the discriminator still read correctly on deserialization, and that the change does not break the existing kotlin-server sample tests.

Written by the indexing model from the issue text.

Description

Issue: Bug
Bug Report Checklist
  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

For kotlin-server with library=jaxrs-spec, a oneOf schema with a discriminator whose propertyName is also declared as a required property on every subtype produces a Jackson model where the discriminator key is written twice in the serialized JSON.

Root cause: kotlin-server/typeInfoAnnotation.mustache renders

@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "...", visible = true)

on the polymorphic interface/base class. As.PROPERTY tells Jackson to inject the discriminator value during serialization. But because the subtype also declares a real Kotlin property with the same name (which is how openapi-generator always models a discriminator that is "visible"/present in properties), Jackson serializes that field too — once via @JsonTypeInfo, once as a normal @JsonProperty. The result is a JSON object with the same key appearing twice, e.g.:

{"item":{"kind":"CAT","kind":"CAT","lives":9}}

This is the same underlying defect already reported for kotlin-client/kotlin-spring in #21733, but the templates for kotlin-server (at least with library=jaxrs-spec) share the same typeInfoAnnotation partial and are affected the same way, with visible=true on the discriminator. Changing As.PROPERTY to As.EXISTING_PROPERTY fixes it (Jackson then writes the value using the existing property instead of injecting an additional one) without touching JsonSubTypes or any other part of the template.

openapi-generator version

7.26.0 (CLI jar), not yet verified against latest master.

OpenAPI declaration file content or url
openapi: 3.0.3
info:
  title: Minimal oneOf discriminator repro
  version: 1.0.0
paths:
  /orders:
    post:
      operationId: createOrder
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Order'
      responses:
        '201':
          description: Created
components:
  schemas:
    Order:
      type: object
      required: [item]
      properties:
        item:
          oneOf:
            - $ref: '#/components/schemas/Cat'
            - $ref: '#/components/schemas/Dog'
          discriminator:
            propertyName: kind
            mapping:
              CAT: '#/components/schemas/Cat'
              DOG: '#/components/schemas/Dog'
    Cat:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [CAT]
        lives:
          type: integer
    Dog:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [DOG]
        barks:
          type: boolean

Note: with stock kotlin-server + library=jaxrs-spec, the oneOf template (oneof_model.mustache) currently only emits typealias Order_item = Any and drops the @JsonTypeInfo/@JsonSubTypes annotations entirely (separate gap — see "Related issues" below), so to actually observe @JsonTypeInfo being applied to the interface, a one-line custom oneof_model.mustache override is needed:

{{#-first}}
{{#discriminator}}
{{>typeInfoAnnotation}}
interface {{classname}}
{{/discriminator}}
{{^discriminator}}
typealias {{classname}} = Any
{{/discriminator}}
{{/-first}}

This override exists in our project precisely to get a real interface + annotations out of oneOf with jaxrs-spec; the duplicate-key bug is in the upstream typeInfoAnnotation.mustache partial that it includes, not in this override.

Generation Details
java -jar openapi-generator-cli-7.24.0.jar generate \
  -g kotlin-server --library jaxrs-spec \
  -i spec.yaml -o out \
  -t templates-with-oneof_model-override \
  --additional-properties=useJakartaEe=true,serializationLibrary=jackson,interfaceOnly=true,enumPropertyNaming=original \
  --global-property=models,supportingFiles=false

Generated OrderItem.kt (annotation import paths shown fully qualified as the generator emits them):

@com.fasterxml.jackson.annotation.JsonTypeInfo(use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY, property = "kind", visible = true)
@com.fasterxml.jackson.annotation.JsonSubTypes(
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Cat::class, name = "CAT"),
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = Dog::class, name = "DOG")
)
interface OrderItem

Generated Cat.kt:

data class Cat(
    @JsonProperty("kind") val kind: Cat.Kind,
    @JsonProperty("lives") val lives: kotlin.Int? = null
) : OrderItem

Serializing Cat(kind = Cat.Kind.CAT, lives = 9) with plain Jackson (ObjectMapper().writeValueAsString(...)):

  • Actual output: {"item":{"kind":"CAT","kind":"CAT","lives":9}} — invalid JSON with a duplicated key (and will throw with JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION enabled, or silently produce malformed/ambiguous JSON otherwise).
  • Expected output: {"item":{"kind":"CAT","lives":9}}
Steps to reproduce
  1. Save the spec above as spec.yaml.
  2. Generate with kotlin-server, library=jaxrs-spec, serializationLibrary=jackson, using a template directory that overrides oneof_model.mustache as shown above (needed only because of the separate typealias Any gap described below).
  3. Compile the generated Cat/Dog/OrderItem classes with jackson-databind on the classpath.
  4. Serialize a Cat instance with a plain ObjectMapper.
  5. Observe the kind key appearing twice in the output JSON.
Related issues/PRs
  • #21733 — [BUG] [kotlin-client][kotlin-spring] Discriminator is serialized twice (same root cause, different generators/libraries; this report covers kotlin-server/jaxrs-spec, which wasn't included there).
  • kotlin-spring works around the equivalent problem with @JsonIgnoreProperties(value = ["kind"], allowSetters = true) on top of the same @JsonTypeInfo(As.PROPERTY, ...), so a similar annotation could also fix kotlin-server instead of changing As.PROPERTY.
  • Separately (not the subject of this report, but relevant to reproducing it): kotlin-server's own libraries/jaxrs-spec/oneof_model.mustache currently renders oneOf schemas as typealias X = Any with no polymorphism/annotations at all, and the allOf-with-discriminator path in the same library references interface_req_var/interface_opt_var templates that don't exist for jaxrs-spec, causing a TemplateNotFoundException. These look like separate, pre-existing gaps in kotlin-server/jaxrs-spec polymorphism support.
Suggest a fix

The fix is a one-token change in modules/openapi-generator/src/main/resources/kotlin-server/typeInfoAnnotation.mustache: replace As.PROPERTY with As.EXISTING_PROPERTY. As.EXISTING_PROPERTY tells Jackson to use the already-declared property for the discriminator value instead of injecting a synthetic one, which is exactly correct here because the generator always also emits that property on the subtype. No other part of the annotation (JsonSubTypes, visible, etc.) needs to change.

Diff:

[email protected](use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY, property = "{{discriminator.propertyBaseName}}", visible = {{#vendorExtensions.x-discriminator-visible-true}}true{{/vendorExtensions.x-discriminator-visible-true}}{{^vendorExtensions.x-discriminator-visible-true}}false{{/vendorExtensions.x-discriminator-visible-true}})
[email protected](use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.EXISTING_PROPERTY, property = "{{discriminator.propertyBaseName}}", visible = {{#vendorExtensions.x-discriminator-visible-true}}true{{/vendorExtensions.x-discriminator-visible-true}}{{^vendorExtensions.x-discriminator-visible-true}}false{{/vendorExtensions.x-discriminator-visible-true}})
 @com.fasterxml.jackson.annotation.JsonSubTypes(
 {{#discriminator.mappedModels}}
     com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = {{modelName}}::class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{vendorExtensions.x-discriminator-value}}{{/vendorExtensions.x-discriminator-value}}"){{^-last}},{{/-last}}
 {{/discriminator.mappedModels}}
 )

Full fixed template for reference:

@com.fasterxml.jackson.annotation.JsonTypeInfo(use = com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME, include = com.fasterxml.jackson.annotation.JsonTypeInfo.As.EXISTING_PROPERTY, property = "{{discriminator.propertyBaseName}}", visible = {{#vendorExtensions.x-discriminator-visible-true}}true{{/vendorExtensions.x-discriminator-visible-true}}{{^vendorExtensions.x-discriminator-visible-true}}false{{/vendorExtensions.x-discriminator-visible-true}})
@com.fasterxml.jackson.annotation.JsonSubTypes(
{{#discriminator.mappedModels}}
    com.fasterxml.jackson.annotation.JsonSubTypes.Type(value = {{modelName}}::class, name = "{{^vendorExtensions.x-discriminator-value}}{{mappingName}}{{/vendorExtensions.x-discriminator-value}}{{#vendorExtensions.x-discriminator-value}}{{vendorExtensions.x-discriminator-value}}{{/vendorExtensions.x-discriminator-value}}"){{^-last}},{{/-last}}
{{/discriminator.mappedModels}}
)

I verified this fix locally: generating with the original template and serializing a Cat instance via plain Jackson produces {"item":{"kind":"CAT","kind":"CAT","lives":9}}; regenerating with only As.PROPERTY → As.EXISTING_PROPERTY changed produces {"item":{"kind":"CAT","lives":9}}, with no other differences in the generated code.

PS: This bug report was partly generated with AI (Sonnet 5.5) by anonymizing a real example from production code I a not allowed to share, sorry.

Dominant language
Java
Stars
26.8k
Forks
7.7k
Avg merge
1d 17h
Merged PRs (30d)
189

Getting set up

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from OpenAPITools/openapi-generator

All issues in OpenAPITools/openapi-generator

Similar issues

More Java issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.