-
-
Notifications
You must be signed in to change notification settings - Fork 7.4k
[kotlin][jvm-okhttp4] Fix multipart/form-data with JSON content-type #22856
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
[kotlin][jvm-okhttp4] Fix multipart/form-data with JSON content-type #22856
Conversation
Fixes OpenAPITools#16457 Fixes two critical bugs in multipart/form-data handling when parts have Content-Type application/json: 1. IllegalArgumentException: OkHttp throws "Unexpected header: Content-Type" because Content-Type was passed in headers map instead of via asRequestBody(mediaType)/toRequestBody(mediaType) parameter. 2. Invalid JSON serialization: Non-file parts with application/json Content-Type were serialized using toString() instead of proper JSON serialization, producing invalid output like: "MyObject(field1=value, field2=123)" instead of '{"field1":"value","field2":123}' Changes: - Filter Content-Type from headers before passing to OkHttp - Check part Content-Type and use appropriate serializer (JSON vs toString) - Add integration tests with echo server to verify fix - Support all serialization libraries (gson, moshi, jackson, kotlinx) Fixes issues with multipart endpoints that mix file uploads with JSON metadata, common in REST APIs for document/image uploads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
26 issues found across 62 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/BodyApi.md">
<violation number="1" location="samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/BodyApi.md:25">
P3: The Kotlin example declares `metadata` with an `=` but provides no value, producing invalid code that users cannot compile or run.</violation>
<violation number="2" location="samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/BodyApi.md:40">
P3: Malformed Markdown table: header/separator rows come after the first data row, so `metadata` is rendered outside the table.</violation>
</file>
<file name="samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:106">
P2: Multipart file parts drop explicit Content-Type from headers and never pass it to asRequestBody, so spec-defined media types are lost in favor of guessed types.</violation>
</file>
<file name="samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: Explicit Content-Type header for file parts is now filtered out and never applied to the RequestBody, so callers cannot override the guessed media type.</violation>
</file>
<file name="samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: Multipart file parts ignore an explicit Content-Type header and always fall back to guessing from the file extension, which can override the caller’s intended media type.</violation>
</file>
<file name="samples/client/petstore/kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: Explicit Content-Type for file parts is discarded; the file overload always guesses media type from filename, ignoring user/API-provided Content-Type headers.</violation>
</file>
<file name="samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: File multipart parts drop the caller-provided Content-Type and always guess from filename, so explicit part Content-Types (e.g., from spec encoding) are ignored.</violation>
</file>
<file name="samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ByteArrayAdapter.kt">
<violation number="1" location="samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ByteArrayAdapter.kt:15">
P2: ByteArray is serialized via String(value)/toByteArray() using the platform default charset, which is lossy for arbitrary binary data and can corrupt non‑text bytes. ByteArray JSON should be base64‑encoded to preserve data.</violation>
</file>
<file name="samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiAbstractions.kt">
<violation number="1" location="samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiAbstractions.kt:8">
P2: collectionDelimiter handles "pipe"/"space" but standard OpenAPI collectionFormat values are "pipes" and "ssv", causing no delimiter and malformed serialized arrays.</violation>
</file>
<file name="samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: File multipart parts ignore any user-specified Content-Type header; the header is stripped but the media type is always guessed from the filename. This can send the wrong part Content-Type when the OpenAPI spec or caller specifies it explicitly.</violation>
</file>
<file name="samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:105">
P2: File multipart parts ignore an explicit Content-Type header and always use a guessed media type, so OpenAPI-specified content types for file parts can be lost.</violation>
</file>
<file name="samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: Multipart file parts now drop any caller-provided Content-Type and always use extension-based guessing, which ignores explicit MIME types from the OpenAPI spec or user headers.</violation>
</file>
<file name="samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: File multipart parts drop a provided Content-Type header and always rely on extension-based guessing, so spec-defined part media types are lost.</violation>
</file>
<file name="samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: Explicit Content-Type headers for multipart file parts are discarded after filtering, so the file part always uses the guessed media type instead of the caller-specified one.</violation>
</file>
<file name="samples/client/others/kotlin-jvm-okhttp-path-comments/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/others/kotlin-jvm-okhttp-path-comments/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: File multipart parts discard an explicit Content-Type header and always guess from the filename, so user-provided content types are ignored for file uploads.</violation>
</file>
<file name="samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:104">
P2: Explicit Content-Type header for multipart File parts is ignored; media type is always guessed from file extension, so per-part content types in headers (e.g., from OpenAPI encoding) are discarded.</violation>
</file>
<file name="samples/client/others/kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/others/kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: Multipart file parts ignore explicit Content-Type headers; only filename-based guessing is used, so caller/spec-provided part content types are lost.</violation>
</file>
<file name="samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: File multipart parts discard an explicit Content-Type header and always use a guessed media type from the filename, so content types defined in part headers are ignored.</violation>
</file>
<file name="samples/client/petstore/kotlin-bigdecimal-default-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-bigdecimal-default-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: Explicit Content-Type headers for multipart file parts are stripped and never used; the file media type is always guessed from the filename, so provided content types are ignored.</violation>
</file>
<file name="samples/client/petstore/kotlin-name-parameter-mappings/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-name-parameter-mappings/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: Explicit Content-Type for file parts is stripped from headers but never applied to the RequestBody, so multipart file uploads always use filename-based guessing and ignore spec-defined media types.</violation>
</file>
<file name="samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: User-provided `Content-Type` for file multipart parts is stripped and ignored; file media type is always guessed from filename, so spec-defined content types are lost.</violation>
</file>
<file name="samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: Multipart file parts discard explicit Content-Type headers but never use that value for the RequestBody media type, so caller-specified part content types are ignored and the file may be sent as application/octet-stream.</violation>
</file>
<file name="samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: Explicit `Content-Type` for file parts is discarded: the file overload filters it out and always uses filename guessing, so user-specified part content types are ignored.</violation>
</file>
<file name="samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:103">
P2: Multipart file parts ignore an explicit Content-Type header, always using a filename guess instead. This can send incorrect media types for file uploads when the OpenAPI spec provides a specific Content-Type.</violation>
</file>
<file name="samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt">
<violation number="1" location="samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt:105">
P2: File multipart parts discard explicit Content-Type headers and always use a guessed media type, so per‑part content types configured in headers are ignored.</violation>
</file>
<file name="modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache">
<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache:170">
P2: Multipart JSON parts using Kotlinx Serialization will throw at runtime because `encodeToString(obj)` is invoked with type‑erased `Any` (no serializer), which requires polymorphic registration.</violation>
</file>
Since this is your first cubic review, here's how it works:
- cubic automatically reviews your code and comments on bugs and improvements
- Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
- Ask questions if you need clarification on any suggestion
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| val partHeaders = headers.toMutableMap() + | ||
| // Filter out Content-Type from headers as OkHttp requires it to be passed | ||
| // separately via asRequestBody(mediaType), not in the headers map | ||
| val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Multipart file parts drop explicit Content-Type from headers and never pass it to asRequestBody, so spec-defined media types are lost in favor of guessed types.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt, line 106:
<comment>Multipart file parts drop explicit Content-Type from headers and never pass it to asRequestBody, so spec-defined media types are lost in favor of guessed types.</comment>
<file context>
@@ -101,7 +101,9 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
- val partHeaders = headers.toMutableMap() +
+ // Filter out Content-Type from headers as OkHttp requires it to be passed
+ // separately via asRequestBody(mediaType), not in the headers map
+ val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
</file context>
| val partHeaders = headers.toMutableMap() + | ||
| // Filter out Content-Type from headers as OkHttp requires it to be passed | ||
| // separately via asRequestBody(mediaType), not in the headers map | ||
| val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Explicit Content-Type header for file parts is now filtered out and never applied to the RequestBody, so callers cannot override the guessed media type.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt, line 103:
<comment>Explicit Content-Type header for file parts is now filtered out and never applied to the RequestBody, so callers cannot override the guessed media type.</comment>
<file context>
@@ -98,7 +98,9 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
- val partHeaders = headers.toMutableMap() +
+ // Filter out Content-Type from headers as OkHttp requires it to be passed
+ // separately via asRequestBody(mediaType), not in the headers map
+ val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
</file context>
| val partHeaders = headers.toMutableMap() + | ||
| // Filter out Content-Type from headers as OkHttp requires it to be passed | ||
| // separately via asRequestBody(mediaType), not in the headers map | ||
| val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Multipart file parts ignore an explicit Content-Type header and always fall back to guessing from the file extension, which can override the caller’s intended media type.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt, line 103:
<comment>Multipart file parts ignore an explicit Content-Type header and always fall back to guessing from the file extension, which can override the caller’s intended media type.</comment>
<file context>
@@ -98,7 +98,9 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
- val partHeaders = headers.toMutableMap() +
+ // Filter out Content-Type from headers as OkHttp requires it to be passed
+ // separately via asRequestBody(mediaType), not in the headers map
+ val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
</file context>
| val partHeaders = headers.toMutableMap() + | ||
| // Filter out Content-Type from headers as OkHttp requires it to be passed | ||
| // separately via asRequestBody(mediaType), not in the headers map | ||
| val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Explicit Content-Type for file parts is discarded; the file overload always guesses media type from filename, ignoring user/API-provided Content-Type headers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt, line 103:
<comment>Explicit Content-Type for file parts is discarded; the file overload always guesses media type from filename, ignoring user/API-provided Content-Type headers.</comment>
<file context>
@@ -98,7 +98,9 @@ public open class ApiClient(public val baseUrl: String, public val client: Call.
- val partHeaders = headers.toMutableMap() +
+ // Filter out Content-Type from headers as OkHttp requires it to be passed
+ // separately via asRequestBody(mediaType), not in the headers map
+ val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
</file context>
| val partHeaders = headers.toMutableMap() + | ||
| // Filter out Content-Type from headers as OkHttp requires it to be passed | ||
| // separately via asRequestBody(mediaType), not in the headers map | ||
| val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: File multipart parts drop the caller-provided Content-Type and always guess from filename, so explicit part Content-Types (e.g., from spec encoding) are ignored.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt, line 103:
<comment>File multipart parts drop the caller-provided Content-Type and always guess from filename, so explicit part Content-Types (e.g., from spec encoding) are ignored.</comment>
<file context>
@@ -98,7 +98,9 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
- val partHeaders = headers.toMutableMap() +
+ // Filter out Content-Type from headers as OkHttp requires it to be passed
+ // separately via asRequestBody(mediaType), not in the headers map
+ val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
</file context>
| val partHeaders = headers.toMutableMap() + | ||
| // Filter out Content-Type from headers as OkHttp requires it to be passed | ||
| // separately via asRequestBody(mediaType), not in the headers map | ||
| val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Multipart file parts ignore an explicit Content-Type header, always using a filename guess instead. This can send incorrect media types for file uploads when the OpenAPI spec provides a specific Content-Type.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt, line 103:
<comment>Multipart file parts ignore an explicit Content-Type header, always using a filename guess instead. This can send incorrect media types for file uploads when the OpenAPI spec provides a specific Content-Type.</comment>
<file context>
@@ -98,7 +98,9 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
- val partHeaders = headers.toMutableMap() +
+ // Filter out Content-Type from headers as OkHttp requires it to be passed
+ // separately via asRequestBody(mediaType), not in the headers map
+ val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
</file context>
| val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() + | ||
| ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") | ||
| val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: File multipart parts discard explicit Content-Type headers and always use a guessed media type, so per‑part content types configured in headers are ignored.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt, line 105:
<comment>File multipart parts discard explicit Content-Type headers and always use a guessed media type, so per‑part content types configured in headers are ignored.</comment>
<file context>
@@ -100,7 +100,9 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
- val partHeaders = headers.toMutableMap() +
+ // Filter out Content-Type from headers as OkHttp requires it to be passed
+ // separately via asRequestBody(mediaType), not in the headers map
+ val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
</file context>
| val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() + | |
| ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") | |
| val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() | |
| val partContentType = headers["Content-Type"] | |
| val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() + | |
| ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") | |
| val fileMediaType = (partContentType ?: guessContentTypeFromFile(file)).toMediaTypeOrNull() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "always guessing" behavior is a separate pre-existing issue, not introduced by this PR. As to whether this suggested change of (partContentType ?: guessContentTypeFromFile(file)) is correct, I'm not sure. I think it might need it's own investigation and PR.
This remark covers the various duplicate comments below.
| Serializer.jacksonObjectMapper.writeValueAsString(obj) | ||
| {{/jackson}} | ||
| {{#kotlinx_serialization}} | ||
| Serializer.kotlinxSerializationJson.encodeToString(obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Multipart JSON parts using Kotlinx Serialization will throw at runtime because encodeToString(obj) is invoked with type‑erased Any (no serializer), which requires polymorphic registration.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache, line 170:
<comment>Multipart JSON parts using Kotlinx Serialization will throw at runtime because `encodeToString(obj)` is invoked with type‑erased `Any` (no serializer), which requires polymorphic registration.</comment>
<file context>
@@ -148,11 +150,31 @@ import com.squareup.moshi.adapter
+ Serializer.jacksonObjectMapper.writeValueAsString(obj)
+ {{/jackson}}
+ {{#kotlinx_serialization}}
+ Serializer.kotlinxSerializationJson.encodeToString(obj)
+ {{/kotlinx_serialization}}
+ } else {
</file context>
| //import org.openapitools.client.models.* | ||
|
|
||
| val apiInstance = BodyApi() | ||
| val metadata : FileMetadata = // FileMetadata | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: The Kotlin example declares metadata with an = but provides no value, producing invalid code that users cannot compile or run.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/BodyApi.md, line 25:
<comment>The Kotlin example declares `metadata` with an `=` but provides no value, producing invalid code that users cannot compile or run.</comment>
<file context>
@@ -0,0 +1,57 @@
+//import org.openapitools.client.models.*
+
+val apiInstance = BodyApi()
+val metadata : FileMetadata = // FileMetadata |
+val file : java.io.File = BINARY_DATA_HERE // java.io.File | File to upload
+try {
</file context>
| ``` | ||
|
|
||
| ### Parameters | ||
| | **metadata** | [**FileMetadata**](FileMetadata.md)| | | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: Malformed Markdown table: header/separator rows come after the first data row, so metadata is rendered outside the table.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/BodyApi.md, line 40:
<comment>Malformed Markdown table: header/separator rows come after the first data row, so `metadata` is rendered outside the table.</comment>
<file context>
@@ -0,0 +1,57 @@
+```
+
+### Parameters
+| **metadata** | [**FileMetadata**](FileMetadata.md)| | |
+| Name | Type | Description | Notes |
+| ------------- | ------------- | ------------- | ------------- |
</file context>
Description
Fixes #16457
Fixes two bugs in the Kotlin
jvm-okhttp4client when handlingmultipart/form-datarequests with parts that haveContent-Type: application/json.Developed with assistance from Claude Code (Sonnet 3.5)
Problem 1: IllegalArgumentException
When a multipart part includes
Content-Typein its headers (via OpenAPIencodingdeclarations), OkHttp throws:Root cause: OkHttp's MultipartBody API requires
Content-Typeto be passed via theasRequestBody(mediaType)ortoRequestBody(mediaType)parameter, not in the headers map passed toaddPart().Problem 2: Invalid JSON Serialization
When a part has
Content-Type: application/json, the object is serialized using Kotlin'stoString()instead of proper JSON serialization.Example of broken output:
Expected output:
Solution
Modified the
addPartToMultiPart()helper methods inApiClient.kt.mustacheto:parameterToString()as beforetoRequestBody()/asRequestBody()The fix supports all serialization libraries: gson, moshi, jackson, and kotlinx-serialization.
Testing
Added comprehensive integration tests:
New test spec:
modules/openapi-generator/src/test/resources/3_0/kotlin/echo_multipart_json.yamlencodingto declare Content-TypesNew sample client:
samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/MultipartJsonTest.ktwith tests that:CI integration: Updated
.github/workflows/samples-kotlin-echo-api.yamlto run the new testsTest Results
Before fix:
After fix:
Regression check: Existing
kotlin-jvm-okhttptests still pass ✅Use Case
This fix is essential for REST APIs that combine file uploads with structured metadata, such as:
cc @jimschubert (Kotlin maintainer)
PR checklist
Commit all changed files.
This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
These must match the expectations made by your contribution.
You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example
./bin/generate-samples.sh bin/configs/java*.IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
master(upcoming7.x.0minor release - breaking changes with fallbacks),8.0.x(breaking changes without fallbacks)Summary by cubic
Fixes multipart/form-data handling in the Kotlin jvm-okhttp4 client when a part uses Content-Type: application/json. Prevents OkHttp header errors and serializes JSON parts as real JSON instead of toString().
Written for commit 4e1607e. Summary will update on new commits.