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

Abierto Apto para principiantes
#25,524 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@Inf1mum ya está trabajando en esto.

Desde el 3/11/2026.

  • #25543 de @Inf1mum — abierto

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
62/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
java, kotlin, openapi
Área
api, backend

Línea de trabajo

La anotación se renderiza desde modules/openapi-generator/src/main/resources/kotlin-server/typeInfoAnnotation.mustache, que establece el modo include de JsonTypeInfo en As.PROPERTY. Empieza generando la especificación en el informe con kotlin-server y library jaxrs-spec, y luego serializa una instancia de Cat con Jackson estándar para confirmar la clave kind duplicada. Se considera terminado cuando la salida contiene kind una sola vez, el discriminador sigue leyéndose correctamente al deserializar y el cambio no rompe las pruebas existentes del sample de kotlin-server.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

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.

Lenguaje dominante
Java
Estrellas
26.8k
Forks
7.8k
Merge medio
2 d 1 h
PR fusionados (30 d)
197

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de OpenAPITools/openapi-generator

Todos los issues de OpenAPITools/openapi-generator

Issues similares

Más issues de Java

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.