[BUG][KOTLIN-SERVER] typeInfoAnnotation.mustache uses As.PROPERTY → discriminator property serialized twice
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 62/100
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
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 withJsonGenerator.Feature.STRICT_DUPLICATE_DETECTIONenabled, or silently produce malformed/ambiguous JSON otherwise). - Expected output:
{"item":{"kind":"CAT","lives":9}}
Steps to reproduce
- Save the spec above as
spec.yaml. - Generate with
kotlin-server,library=jaxrs-spec,serializationLibrary=jackson, using a template directory that overridesoneof_model.mustacheas shown above (needed only because of the separatetypealias Anygap described below). - Compile the generated
Cat/Dog/OrderItemclasses withjackson-databindon the classpath. - Serialize a
Catinstance with a plainObjectMapper. - Observe the
kindkey 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 fixkotlin-serverinstead of changingAs.PROPERTY. - Separately (not the subject of this report, but relevant to reproducing it):
kotlin-server's ownlibraries/jaxrs-spec/oneof_model.mustachecurrently rendersoneOfschemas astypealias X = Anywith no polymorphism/annotations at all, and theallOf-with-discriminator path in the same library referencesinterface_req_var/interface_opt_vartemplates that don't exist forjaxrs-spec, causing aTemplateNotFoundException. These look like separate, pre-existing gaps inkotlin-server/jaxrs-specpolymorphism 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
Starts the project's dev container in your browser, under your own GitHub account.
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from OpenAPITools/openapi-generator
-
[REQ][JAVA][resteasy] Support for Jackson 3 / useJackson3Possibly taken A pull request linked to this issue is open or already merged. OpenEnhancement: Feature
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
OpenAPITools/openapi-generator#25364 ·
Maintainers usually reply within 1 day
-
[BUG][RUST] chrono date-time path/query/header params are not serialized as RFC 3339Possibly taken @teovillanueva claimed this 17 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
OpenAPITools/openapi-generator#25297 ·
Maintainers usually reply within 1 day
-
[BUG][RUST] Reqwest - encoding of spaces in path parametersPossibly taken @GlenWright claimed this 20 days ago. OpenIssue: Bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
OpenAPITools/openapi-generator#25257 · 3 comments ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
OpenAPITools/openapi-generator#25237 ·
Maintainers usually reply within 1 day
-
[BUG] OpenApiEvaluator reports "default value not in enum" for numerically equal values with different scale (e.g. 1.5 and 1.50)Possibly taken @mohitduhan19 claimed this 21 days ago. OpenIssue: Bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
OpenAPITools/openapi-generator#25233 ·
Maintainers usually reply within 1 day
All issues in OpenAPITools/openapi-generator
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
objectionary/lints#1640 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
openhab/openhab-addons#22168 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
wildfly/wildfly-channel#446 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Maintainers usually reply within 1 day
-
5.0.x backport enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 63/100
spring-cloud/spring-cloud-function#1507 ·
Maintainers usually reply within 1 day