Bug Report Checklist
Description
For an operation whose consumes list has more than one media type (e.g.
application/json and multipart/form-data) but that has no formData
parameters — i.e. it uses a plain body parameter and multipart is only listed
because the same endpoint also accepts a file upload variant — the
kotlin generator with library=jvm-spring-restclient emits one
localVariableHeaders["Content-Type"] = "..." assignment per declared consumes
entry, all writing to the same map key. The last entry in consumes always
wins at runtime, regardless of which content type actually matches the body being
sent.
This means every call to the generated method sends the same Content-Type
(whichever media type happens to be listed last in the spec) even when the caller
supplies a JSON body object, not a multipart form. Against a real server this causes
the server to attempt to parse a JSON payload as multipart form data and fail (in our
case, against Alfresco Content Services, it throws a ClassCastException trying to
cast the JSON-deserialized body to a Map).
We hit this generating a client for Alfresco's public
POST /nodes/{nodeId}/children operation
(https://github.com/Alfresco/rest-api-explorer), which legitimately declares:
consumes:
- "application/json"
- "multipart/form-data"
because the endpoint supports both a JSON body and a multipart file-upload body,
but the generated client only ever builds the JSON-body call in our usage — yet
the header is force-set to multipart/form-data.
openapi-generator version
Confirmed present in 7.24.0 (latest stable release) and 7.21.0
(byte-for-byte identical generated output between the two). Also independently
rebuilt and tested against the latest master, commit
c3cde9bd5469e47ed58500b6235e54cc368ce432 / version 7.25.0-SNAPSHOT — again
byte-for-byte identical; the bug is still present.
OpenAPI declaration file content or url
Minimal Swagger 2.0 spec that reproduces the issue (mirrors the real Alfresco
operation shape — body param, multiple consumes, no formData params):
swagger: "2.0"
info:
title: "Multi-consumes Content-Type bug repro"
version: "1.0"
basePath: "/api"
paths:
/nodes/{nodeId}/children:
post:
tags:
- "nodes"
summary: "Create a node"
operationId: "createNode"
parameters:
- name: "nodeId"
in: "path"
required: true
type: "string"
- in: "body"
name: "nodeBodyCreate"
description: "The node information to create."
required: true
schema:
$ref: "#/definitions/NodeBodyCreate"
consumes:
- "application/json"
- "multipart/form-data"
produces:
- "application/json"
responses:
"201":
description: "Successful response"
schema:
$ref: "#/definitions/NodeEntry"
definitions:
NodeBodyCreate:
type: "object"
properties:
name:
type: "string"
nodeType:
type: "string"
NodeEntry:
type: "object"
properties:
id:
type: "string"
Generation Details
Gradle plugin invocation (equivalent CLI: generate -g kotlin --library jvm-spring-restclient -i openapi.yaml -o out --additional-properties serializationLibrary=jackson,enumPropertyNaming=UPPERCASE,useSpringBoot3=true):
plugins {
id("org.openapi.generator") version "7.21.0"
}
openApiGenerate {
generatorName = "kotlin"
library = "jvm-spring-restclient"
inputSpec.set("$projectDir/openapi.yaml")
outputDir.set("$projectDir/out")
apiPackage = "repro.api"
modelPackage = "repro.model"
packageName = "repro"
configOptions = mapOf(
"serializationLibrary" to "jackson",
"enumPropertyNaming" to "UPPERCASE",
"useSpringBoot3" to "true",
)
}
Steps to reproduce
- Save the spec above as
openapi.yaml.
- Generate with
generatorName=kotlin, library=jvm-spring-restclient (config as
above).
- Open the generated
NodesApi.kt and inspect createNodeRequestConfig().
Actual output
fun createNodeRequestConfig(nodeId: kotlin.String, nodeBodyCreate: NodeBodyCreate) : RequestConfig<NodeBodyCreate> {
val localVariableBody = nodeBodyCreate
val localVariableQuery = mutableMapOf<kotlin.String, kotlin.collections.List<kotlin.String>>()
val localVariableHeaders: MutableMap<String, String> = mutableMapOf()
localVariableHeaders["Content-Type"] = "application/json"
localVariableHeaders["Content-Type"] = "multipart/form-data"
localVariableHeaders["Accept"] = "application/json"
...
Content-Type is unconditionally sent as multipart/form-data, even though the
method takes a single JSON-serializable NodeBodyCreate body and no form/multipart
parameters exist at all.
Expected output
Since the operation has no formData parameters, only one Content-Type should be
emitted — the one that actually matches the body being constructed, e.g.:
localVariableHeaders["Content-Type"] = "application/json"
(the first non-multipart entry in consumes, or otherwise a single, well-defined
choice — see "Suggest a fix" below for options).
Related issues/PRs
Searched open and closed issues; the closest related reports are about different
generators/symptoms and are not duplicates of this one:
None address the kotlin/jvm-spring-restclient request-side Content-Type
overwrite for multi-consumes operations without form params.
Suggest a fix
Root cause is in
modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache,
around:
{{^hasFormParams}}{{#hasConsumes}}{{#consumes}}localVariableHeaders["Content-Type"] = "{{{mediaType}}}"
{{/consumes}}{{/hasConsumes}}{{/hasFormParams}}
This iterates over all consumes entries and assigns Content-Type
unconditionally for each, so the last one always wins. Two possible fixes:
- Minimal/safe: only emit the assignment for the first
consumes entry
({{#consumes.0}}...{{/consumes.0}} instead of {{#consumes}}...{{/consumes}}),
matching the existing hasFormParams branch just above it which already uses
{{#consumes.0}}.
- More correct: skip media types that don't match the body actually being
serialized (e.g. skip multipart/form-data/application/x-www-form-urlencoded
in this branch, since this branch by definition runs only when
hasFormParams is false and thus no multipart/form body is being built).
Happy to open a PR with option 1 (smallest change, consistent with the sibling
hasFormParams branch's existing use of consumes.0) if maintainers confirm the
preferred approach.
Bug Report Checklist
Description
For an operation whose
consumeslist has more than one media type (e.g.application/jsonandmultipart/form-data) but that has noformDataparameters — i.e. it uses a plain
bodyparameter and multipart is only listedbecause the same endpoint also accepts a file upload variant — the
kotlingenerator withlibrary=jvm-spring-restclientemits onelocalVariableHeaders["Content-Type"] = "..."assignment per declaredconsumesentry, all writing to the same map key. The last entry in
consumesalwayswins at runtime, regardless of which content type actually matches the body being
sent.
This means every call to the generated method sends the same
Content-Type(whichever media type happens to be listed last in the spec) even when the caller
supplies a JSON body object, not a multipart form. Against a real server this causes
the server to attempt to parse a JSON payload as multipart form data and fail (in our
case, against Alfresco Content Services, it throws a
ClassCastExceptiontrying tocast the JSON-deserialized body to a
Map).We hit this generating a client for Alfresco's public
POST /nodes/{nodeId}/childrenoperation(https://github.com/Alfresco/rest-api-explorer), which legitimately declares:
because the endpoint supports both a JSON body and a multipart file-upload body,
but the generated client only ever builds the JSON-body call in our usage — yet
the header is force-set to
multipart/form-data.openapi-generator version
Confirmed present in 7.24.0 (latest stable release) and 7.21.0
(byte-for-byte identical generated output between the two). Also independently
rebuilt and tested against the latest
master, commitc3cde9bd5469e47ed58500b6235e54cc368ce432/ version7.25.0-SNAPSHOT— againbyte-for-byte identical; the bug is still present.
OpenAPI declaration file content or url
Minimal Swagger 2.0 spec that reproduces the issue (mirrors the real Alfresco
operation shape —
bodyparam, multipleconsumes, noformDataparams):Generation Details
Gradle plugin invocation (equivalent CLI:
generate -g kotlin --library jvm-spring-restclient -i openapi.yaml -o out --additional-properties serializationLibrary=jackson,enumPropertyNaming=UPPERCASE,useSpringBoot3=true):plugins { id("org.openapi.generator") version "7.21.0" } openApiGenerate { generatorName = "kotlin" library = "jvm-spring-restclient" inputSpec.set("$projectDir/openapi.yaml") outputDir.set("$projectDir/out") apiPackage = "repro.api" modelPackage = "repro.model" packageName = "repro" configOptions = mapOf( "serializationLibrary" to "jackson", "enumPropertyNaming" to "UPPERCASE", "useSpringBoot3" to "true", ) }Steps to reproduce
openapi.yaml.generatorName=kotlin,library=jvm-spring-restclient(config asabove).
NodesApi.ktand inspectcreateNodeRequestConfig().Actual output
Content-Typeis unconditionally sent asmultipart/form-data, even though themethod takes a single JSON-serializable
NodeBodyCreatebody and no form/multipartparameters exist at all.
Expected output
Since the operation has no
formDataparameters, only oneContent-Typeshould beemitted — the one that actually matches the body being constructed, e.g.:
(the first non-multipart entry in
consumes, or otherwise a single, well-definedchoice — see "Suggest a fix" below for options).
Related issues/PRs
Searched open and closed issues; the closest related reports are about different
generators/symptoms and are not duplicates of this one:
[BUG][kotlin-spring] Multipart with JSON array broken[BUG][Kotlin-Spring] Wrong parameter type for multipart-form-data file upload when using kotlin-spring with reactive[BUG][kotlin-spring] wrong field name and type for multipart[JAVA][RESTCLIENT] ... Maven plugin 7.22.0 produces invalid client code to get application/octet-stream(responseAcceptside,javagenerator)[BUG][JAVA][webclient] UnsupportedMediaTypeException if multiple consumes media type and unknown type in first position(java/webclientlibrary, different template)None address the
kotlin/jvm-spring-restclientrequest-sideContent-Typeoverwrite for multi-
consumesoperations without form params.Suggest a fix
Root cause is in
modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache,around:
This iterates over all
consumesentries and assignsContent-Typeunconditionally for each, so the last one always wins. Two possible fixes:
consumesentry(
{{#consumes.0}}...{{/consumes.0}}instead of{{#consumes}}...{{/consumes}}),matching the existing
hasFormParamsbranch just above it which already uses{{#consumes.0}}.serialized (e.g. skip
multipart/form-data/application/x-www-form-urlencodedin this branch, since this branch by definition runs only when
hasFormParamsis false and thus no multipart/form body is being built).Happy to open a PR with option 1 (smallest change, consistent with the sibling
hasFormParamsbranch's existing use ofconsumes.0) if maintainers confirm thepreferred approach.