Skip to content

Commit c58de05

Browse files
committed
Fix kotlinx.serialization multipart by adding serializer lambda to PartConfig
1 parent af29043 commit c58de05

82 files changed

Lines changed: 983 additions & 184 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ samples/client/petstore/kotlin*/src/main/kotlin/test/
229229
samples/client/petstore/kotlin*/build/
230230
samples/server/others/kotlin-server/jaxrs-spec/build/
231231
samples/client/echo_api/kotlin-jvm-spring-3-restclient/build/
232+
samples/client/echo_api/kotlin-jvm-spring-3-webclient/build/
232233
samples/client/echo_api/kotlin-jvm-okhttp/build/
233234
samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/build/
234235

modules/openapi-generator/src/main/resources/kotlin-client/infrastructure/PartConfig.kt.mustache

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@ package {{packageName}}.infrastructure
44
* Defines a config object for a given part of a multi-part request.
55
* NOTE: Headers is a Map<String,String> because rfc2616 defines
66
* multi-valued headers as csv-only.
7+
*
8+
* @property headers The headers for this part
9+
* @property body The body content for this part
10+
* @property serializer Optional custom serializer for JSON content. When provided, this will be
11+
* used instead of the default serialization for parts with application/json
12+
* content-type. This allows capturing type information at the call site to
13+
* avoid issues with type erasure in kotlinx.serialization.
714
*/
815
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class PartConfig<T>(
916
val headers: MutableMap<String, String> = mutableMapOf(),
10-
val body: T? = null
17+
val body: T? = null,
18+
val serializer: ((Any?) -> String)? = null
1119
)

modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ import {{packageName}}.infrastructure.RequestMethod
4646
import {{packageName}}.infrastructure.ResponseType
4747
import {{packageName}}.infrastructure.Success
4848
import {{packageName}}.infrastructure.toMultiValue
49+
{{#kotlinx_serialization}}
50+
import {{packageName}}.infrastructure.Serializer
51+
{{/kotlinx_serialization}}
4952

5053
{{#operations}}
5154
{{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}open {{/nonPublicApi}}class {{classname}}(basePath: kotlin.String = defaultBasePath, client: Call.Factory = ApiClient.defaultClient) : ApiClient(basePath, client) {
@@ -199,7 +202,7 @@ import {{packageName}}.infrastructure.toMultiValue
199202
}}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{!
200203
}}{{^hasFormParams}}null{{/hasFormParams}}{{!
201204
}}{{#hasFormParams}}mapOf({{#formParams}}
202-
"{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{!
205+
"{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}}){{#contentType}}{{^isFile}}, serializer = {{#kotlinx_serialization}}{ obj -> Serializer.kotlinxSerializationJson.encodeToString<{{{dataType}}}>(obj as {{{dataType}}}) }{{/kotlinx_serialization}}{{^kotlinx_serialization}}null{{/kotlinx_serialization}}{{/isFile}}{{/contentType}}),{{!
203206
}}{{/formParams}}){{/hasFormParams}}{{!
204207
}}{{/hasBodyParam}}
205208
val localVariableQuery: MultiValueMap = {{^hasQueryParams}}mutableMapOf()

modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,15 @@ import com.squareup.moshi.adapter
159159
*
160160
* @param obj The object to serialize
161161
* @param contentType The Content-Type header value, if any
162+
* @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info)
162163
* @return The serialized string representation
163164
*/
164-
protected inline fun <reified T> serializePartBody(obj: T?, contentType: String?): String {
165+
protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String {
166+
// Use custom serializer if provided (for kotlinx.serialization with captured type info)
167+
if (serializer != null) {
168+
return serializer(obj)
169+
}
170+
165171
return if (contentType?.contains("json") == true) {
166172
{{#moshi}}
167173
Serializer.moshi.adapter(Any::class.java).toJson(obj)
@@ -173,7 +179,9 @@ import com.squareup.moshi.adapter
173179
Serializer.jacksonObjectMapper.writeValueAsString(obj)
174180
{{/jackson}}
175181
{{#kotlinx_serialization}}
176-
Serializer.kotlinxSerializationJson.encodeToString(obj)
182+
// Note: Without a custom serializer, kotlinx.serialization cannot serialize Any?
183+
// The custom serializer should be provided at PartConfig creation to capture type info
184+
parameterToString(obj)
177185
{{/kotlinx_serialization}}
178186
} else {
179187
parameterToString(obj)
@@ -188,13 +196,14 @@ import com.squareup.moshi.adapter
188196
* @param name The field name to add in the request
189197
* @param headers The headers that are in the PartConfig
190198
* @param obj The field name to add in the request
199+
* @param serializer Optional custom serializer for this part
191200
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
192201
* @see requestBody
193202
*/
194-
protected inline fun <reified T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
203+
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: Any?, serializer: ((Any?) -> String)? = null) {
195204
val partContentType = headers["Content-Type"]
196205
val partMediaType = partContentType?.toMediaTypeOrNull()
197-
val partBody = serializePartBody(obj, partContentType)
206+
val partBody = serializePartBody(obj, partContentType, serializer)
198207
addPart(
199208
buildPartHeaders(name, headers),
200209
partBody.toRequestBody(partMediaType)
@@ -219,11 +228,11 @@ import com.squareup.moshi.adapter
219228
if (it is File) {
220229
addPartToMultiPart(name, part.headers, it)
221230
} else {
222-
addPartToMultiPart(name, part.headers, it)
231+
addPartToMultiPart(name, part.headers, it, part.serializer)
223232
}
224233
}
225234
}
226-
else -> addPartToMultiPart(name, part.headers, part.body)
235+
else -> addPartToMultiPart(name, part.headers, part.body, part.serializer)
227236
}
228237
}
229238
}.build()

modules/openapi-generator/src/test/resources/3_0/kotlin/polymorphism.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,35 @@ paths:
3030
name: id
3131
in: path
3232
required: true
33+
'/v1/bird/upload':
34+
post:
35+
tags:
36+
- bird
37+
operationId: upload-bird-with-metadata
38+
requestBody:
39+
content:
40+
multipart/form-data:
41+
schema:
42+
type: object
43+
properties:
44+
metadata:
45+
$ref: '#/components/schemas/bird'
46+
file:
47+
type: string
48+
format: binary
49+
required:
50+
- metadata
51+
- file
52+
encoding:
53+
metadata:
54+
contentType: application/json
55+
responses:
56+
'200':
57+
description: Upload successful
58+
content:
59+
text/plain:
60+
schema:
61+
type: string
3362
components:
3463
schemas:
3564
animal:

samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,15 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
130130
*
131131
* @param obj The object to serialize
132132
* @param contentType The Content-Type header value, if any
133+
* @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info)
133134
* @return The serialized string representation
134135
*/
135-
protected inline fun <reified T> serializePartBody(obj: T?, contentType: String?): String {
136+
protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String {
137+
// Use custom serializer if provided (for kotlinx.serialization with captured type info)
138+
if (serializer != null) {
139+
return serializer(obj)
140+
}
141+
136142
return if (contentType?.contains("json") == true) {
137143
Serializer.moshi.adapter(Any::class.java).toJson(obj)
138144
} else {
@@ -148,13 +154,14 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
148154
* @param name The field name to add in the request
149155
* @param headers The headers that are in the PartConfig
150156
* @param obj The field name to add in the request
157+
* @param serializer Optional custom serializer for this part
151158
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
152159
* @see requestBody
153160
*/
154-
protected inline fun <reified T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
161+
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: Any?, serializer: ((Any?) -> String)? = null) {
155162
val partContentType = headers["Content-Type"]
156163
val partMediaType = partContentType?.toMediaTypeOrNull()
157-
val partBody = serializePartBody(obj, partContentType)
164+
val partBody = serializePartBody(obj, partContentType, serializer)
158165
addPart(
159166
buildPartHeaders(name, headers),
160167
partBody.toRequestBody(partMediaType)
@@ -179,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
179186
if (it is File) {
180187
addPartToMultiPart(name, part.headers, it)
181188
} else {
182-
addPartToMultiPart(name, part.headers, it)
189+
addPartToMultiPart(name, part.headers, it, part.serializer)
183190
}
184191
}
185192
}
186-
else -> addPartToMultiPart(name, part.headers, part.body)
193+
else -> addPartToMultiPart(name, part.headers, part.body, part.serializer)
187194
}
188195
}
189196
}.build()

samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure
44
* Defines a config object for a given part of a multi-part request.
55
* NOTE: Headers is a Map<String,String> because rfc2616 defines
66
* multi-valued headers as csv-only.
7+
*
8+
* @property headers The headers for this part
9+
* @property body The body content for this part
10+
* @property serializer Optional custom serializer for JSON content. When provided, this will be
11+
* used instead of the default serialization for parts with application/json
12+
* content-type. This allows capturing type information at the call site to
13+
* avoid issues with type erasure in kotlinx.serialization.
714
*/
815
data class PartConfig<T>(
916
val headers: MutableMap<String, String> = mutableMapOf(),
10-
val body: T? = null
17+
val body: T? = null,
18+
val serializer: ((Any?) -> String)? = null
1119
)

samples/client/echo_api/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure
44
* Defines a config object for a given part of a multi-part request.
55
* NOTE: Headers is a Map<String,String> because rfc2616 defines
66
* multi-valued headers as csv-only.
7+
*
8+
* @property headers The headers for this part
9+
* @property body The body content for this part
10+
* @property serializer Optional custom serializer for JSON content. When provided, this will be
11+
* used instead of the default serialization for parts with application/json
12+
* content-type. This allows capturing type information at the call site to
13+
* avoid issues with type erasure in kotlinx.serialization.
714
*/
815
data class PartConfig<T>(
916
val headers: MutableMap<String, String> = mutableMapOf(),
10-
val body: T? = null
17+
val body: T? = null,
18+
val serializer: ((Any?) -> String)? = null
1119
)

samples/client/echo_api/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure
44
* Defines a config object for a given part of a multi-part request.
55
* NOTE: Headers is a Map<String,String> because rfc2616 defines
66
* multi-valued headers as csv-only.
7+
*
8+
* @property headers The headers for this part
9+
* @property body The body content for this part
10+
* @property serializer Optional custom serializer for JSON content. When provided, this will be
11+
* used instead of the default serialization for parts with application/json
12+
* content-type. This allows capturing type information at the call site to
13+
* avoid issues with type erasure in kotlinx.serialization.
714
*/
815
data class PartConfig<T>(
916
val headers: MutableMap<String, String> = mutableMapOf(),
10-
val body: T? = null
17+
val body: T? = null,
18+
val serializer: ((Any?) -> String)? = null
1119
)

samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,15 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
130130
*
131131
* @param obj The object to serialize
132132
* @param contentType The Content-Type header value, if any
133+
* @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info)
133134
* @return The serialized string representation
134135
*/
135-
protected inline fun <reified T> serializePartBody(obj: T?, contentType: String?): String {
136+
protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String {
137+
// Use custom serializer if provided (for kotlinx.serialization with captured type info)
138+
if (serializer != null) {
139+
return serializer(obj)
140+
}
141+
136142
return if (contentType?.contains("json") == true) {
137143
Serializer.moshi.adapter(Any::class.java).toJson(obj)
138144
} else {
@@ -148,13 +154,14 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
148154
* @param name The field name to add in the request
149155
* @param headers The headers that are in the PartConfig
150156
* @param obj The field name to add in the request
157+
* @param serializer Optional custom serializer for this part
151158
* @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on
152159
* @see requestBody
153160
*/
154-
protected inline fun <reified T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
161+
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: Any?, serializer: ((Any?) -> String)? = null) {
155162
val partContentType = headers["Content-Type"]
156163
val partMediaType = partContentType?.toMediaTypeOrNull()
157-
val partBody = serializePartBody(obj, partContentType)
164+
val partBody = serializePartBody(obj, partContentType, serializer)
158165
addPart(
159166
buildPartHeaders(name, headers),
160167
partBody.toRequestBody(partMediaType)
@@ -179,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie
179186
if (it is File) {
180187
addPartToMultiPart(name, part.headers, it)
181188
} else {
182-
addPartToMultiPart(name, part.headers, it)
189+
addPartToMultiPart(name, part.headers, it, part.serializer)
183190
}
184191
}
185192
}
186-
else -> addPartToMultiPart(name, part.headers, part.body)
193+
else -> addPartToMultiPart(name, part.headers, part.body, part.serializer)
187194
}
188195
}
189196
}.build()

0 commit comments

Comments
 (0)