diff --git a/.github/workflows/samples-kotlin-echo-api.yaml b/.github/workflows/samples-kotlin-echo-api.yaml index 683f06f070bf..a5e73a4ff4ee 100644 --- a/.github/workflows/samples-kotlin-echo-api.yaml +++ b/.github/workflows/samples-kotlin-echo-api.yaml @@ -20,6 +20,7 @@ jobs: - samples/client/echo_api/kotlin-jvm-spring-3-restclient - samples/client/echo_api/kotlin-model-prefix-type-mappings - samples/client/echo_api/kotlin-jvm-okhttp + - samples/client/echo_api/kotlin-jvm-okhttp-multipart-json steps: - uses: actions/checkout@v5 - uses: actions/setup-java@v5 diff --git a/.gitignore b/.gitignore index 203a85a3de8b..a11c07212928 100644 --- a/.gitignore +++ b/.gitignore @@ -229,7 +229,9 @@ samples/client/petstore/kotlin*/src/main/kotlin/test/ samples/client/petstore/kotlin*/build/ samples/server/others/kotlin-server/jaxrs-spec/build/ samples/client/echo_api/kotlin-jvm-spring-3-restclient/build/ +samples/client/echo_api/kotlin-jvm-spring-3-webclient/build/ samples/client/echo_api/kotlin-jvm-okhttp/build/ +samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/build/ # haskell .stack-work diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/infrastructure/PartConfig.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/infrastructure/PartConfig.kt.mustache index 36c970b7f7ca..d802810551a3 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/infrastructure/PartConfig.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/infrastructure/PartConfig.kt.mustache @@ -4,8 +4,16 @@ package {{packageName}}.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ {{#nonPublicApi}}internal {{/nonPublicApi}}{{^nonPublicApi}}{{#explicitApi}}public {{/explicitApi}}{{/nonPublicApi}}data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache index 669dd54ee00e..8c2213e586f9 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache @@ -46,6 +46,9 @@ import {{packageName}}.infrastructure.RequestMethod import {{packageName}}.infrastructure.ResponseType import {{packageName}}.infrastructure.Success import {{packageName}}.infrastructure.toMultiValue +{{#kotlinx_serialization}} +import {{packageName}}.infrastructure.Serializer +{{/kotlinx_serialization}} {{#operations}} {{#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 }}{{#bodyParams}}{{{paramName}}}{{/bodyParams}}{{/hasBodyParam}}{{^hasBodyParam}}{{! }}{{^hasFormParams}}null{{/hasFormParams}}{{! }}{{#hasFormParams}}mapOf({{#formParams}} - "{{#lambda.escapeDollar}}{{{baseName}}}{{/lambda.escapeDollar}}" to PartConfig(body = {{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}, headers = mutableMapOf({{#contentType}}"Content-Type" to "{{contentType}}"{{/contentType}})),{{! + "{{#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}}),{{! }}{{/formParams}}){{/hasFormParams}}{{! }}{{/hasBodyParam}} val localVariableQuery: MultiValueMap = {{^hasQueryParams}}mutableMapOf() diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache index 3e5fe0a2557d..2faa8be0068c 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache @@ -115,6 +115,25 @@ import com.squareup.moshi.adapter return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -127,15 +146,48 @@ import com.squareup.moshi.adapter * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + {{#moshi}} + Serializer.moshi.adapter(Any::class.java).toJson(obj) + {{/moshi}} + {{#gson}} + Serializer.gson.toJson(obj) + {{/gson}} + {{#jackson}} + Serializer.jacksonObjectMapper.writeValueAsString(obj) + {{/jackson}} + {{#kotlinx_serialization}} + // Note: Without a custom serializer, kotlinx.serialization cannot serialize Any? + // The custom serializer should be provided at PartConfig creation to capture type info + parameterToString(obj) + {{/kotlinx_serialization}} + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -144,15 +196,17 @@ import com.squareup.moshi.adapter * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -174,11 +228,11 @@ import com.squareup.moshi.adapter if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache index 8183a976081c..cf5a4fe733e7 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache @@ -11,6 +11,10 @@ import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine import kotlinx.serialization.json.Json import io.ktor.http.ParametersBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.ContentType +import io.ktor.http.content.PartData import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -76,11 +80,31 @@ import kotlinx.serialization.encoding.* {{#formParams}} {{#isArray}} {{{paramName}}}?.onEach { - {{#isFile}}append(it){{/isFile}}{{^isFile}}append("{{{baseName}}}", it){{/isFile}} + {{#isFile}}append(it){{/isFile}}{{^isFile}}append("{{{baseName}}}", it.toString()){{/isFile}} } {{/isArray}} {{^isArray}} - {{{paramName}}}?.apply { {{#isFile}}append({{{baseName}}}){{/isFile}}{{^isFile}}append("{{{baseName}}}", {{^isEnumOrRef}}{{{paramName}}}{{/isEnumOrRef}}{{#isEnumOrRef}}{{{paramName}}}.value{{/isEnumOrRef}}){{/isFile}} } + {{#isFile}} + {{{paramName}}}?.apply { append({{{baseName}}}) } + {{/isFile}} + {{^isFile}} + {{#isPrimitiveType}} + {{#isString}} + {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}) } + {{/isString}} + {{^isString}} + {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}.toString()) } + {{/isString}} + {{/isPrimitiveType}} + {{^isPrimitiveType}} + {{#isEnumOrRef}} + {{{paramName}}}?.apply { append("{{{baseName}}}", {{{paramName}}}.value.toString()) } + {{/isEnumOrRef}} + {{^isEnumOrRef}} + {{{paramName}}}?.apply { append("{{{baseName}}}", ApiClient.JSON_DEFAULT.encodeToString({{{dataType}}}.serializer(), {{{paramName}}})) } + {{/isEnumOrRef}} + {{/isPrimitiveType}} + {{/isFile}} {{/isArray}} {{/formParams}} } diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/echo_multipart_json.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/echo_multipart_json.yaml new file mode 100644 index 000000000000..b6554b7d078e --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/echo_multipart_json.yaml @@ -0,0 +1,69 @@ +openapi: 3.0.0 +servers: + - url: 'http://localhost:3000/' +info: + version: 1.0.0 + title: Echo API for Kotlin Multipart JSON Test + description: Echo server API to test multipart/form-data with JSON content-type + license: + name: Apache-2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' +tags: + - name: body + description: Test body operations +paths: + /body/multipart/formdata/with_json_part: + post: + tags: + - body + summary: Test multipart with JSON part + description: Test multipart/form-data with a part that has Content-Type application/json + operationId: testBodyMultipartFormdataWithJsonPart + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - metadata + - file + properties: + metadata: + $ref: '#/components/schemas/FileMetadata' + file: + type: string + format: binary + description: File to upload + encoding: + metadata: + contentType: application/json + file: + contentType: image/jpeg + responses: + '200': + description: Successful operation + content: + text/plain: + schema: + type: string +components: + schemas: + FileMetadata: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int64 + example: 12345 + name: + type: string + example: test-file + tags: + type: array + items: + type: string + example: ["tag1", "tag2"] diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/polymorphism.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/polymorphism.yaml index 92cd1539bd1a..35fe2b1365f0 100644 --- a/modules/openapi-generator/src/test/resources/3_0/kotlin/polymorphism.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/polymorphism.yaml @@ -30,6 +30,35 @@ paths: name: id in: path required: true + '/v1/bird/upload': + post: + tags: + - bird + operationId: upload-bird-with-metadata + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + metadata: + $ref: '#/components/schemas/bird' + file: + type: string + format: binary + required: + - metadata + - file + encoding: + metadata: + contentType: application/json + responses: + '200': + description: Upload successful + content: + text/plain: + schema: + type: string components: schemas: animal: diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/.openapi-generator-ignore b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/.openapi-generator/FILES b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/.openapi-generator/FILES new file mode 100644 index 000000000000..08fa2c06cf1b --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/.openapi-generator/FILES @@ -0,0 +1,24 @@ +README.md +build.gradle +docs/BodyApi.md +docs/FileMetadata.md +gradle/wrapper/gradle-wrapper.jar +gradle/wrapper/gradle-wrapper.properties +gradlew +gradlew.bat +settings.gradle +src/main/kotlin/org/openapitools/client/apis/BodyApi.kt +src/main/kotlin/org/openapitools/client/infrastructure/ApiAbstractions.kt +src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +src/main/kotlin/org/openapitools/client/infrastructure/ApiResponse.kt +src/main/kotlin/org/openapitools/client/infrastructure/ByteArrayAdapter.kt +src/main/kotlin/org/openapitools/client/infrastructure/Errors.kt +src/main/kotlin/org/openapitools/client/infrastructure/LocalDateAdapter.kt +src/main/kotlin/org/openapitools/client/infrastructure/LocalDateTimeAdapter.kt +src/main/kotlin/org/openapitools/client/infrastructure/OffsetDateTimeAdapter.kt +src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +src/main/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt +src/main/kotlin/org/openapitools/client/infrastructure/RequestMethod.kt +src/main/kotlin/org/openapitools/client/infrastructure/ResponseExtensions.kt +src/main/kotlin/org/openapitools/client/infrastructure/Serializer.kt +src/main/kotlin/org/openapitools/client/models/FileMetadata.kt diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/.openapi-generator/VERSION b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/.openapi-generator/VERSION new file mode 100644 index 000000000000..193a12d6e891 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.20.0-SNAPSHOT diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/README.md b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/README.md new file mode 100644 index 000000000000..91065665a8c5 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/README.md @@ -0,0 +1,61 @@ +# org.openapitools.client - Kotlin client library for Echo API for Kotlin Multipart JSON Test + +Echo server API to test multipart/form-data with JSON content-type + +## Overview +This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [openapi-spec](https://github.com/OAI/OpenAPI-Specification) from a remote server, you can easily generate an API client. + +- API version: 1.0.0 +- Package version: +- Generator version: 7.20.0-SNAPSHOT +- Build package: org.openapitools.codegen.languages.KotlinClientCodegen + +## Requires + +* Kotlin 2.2.20 +* Gradle 8.14 + +## Build + +First, create the gradle wrapper script: + +``` +gradle wrapper +``` + +Then, run: + +``` +./gradlew check assemble +``` + +This runs all tests and packages the library. + +## Features/Implementation Notes + +* Supports JSON inputs/outputs, File inputs, and Form inputs. +* Supports collection formats for query parameters: csv, tsv, ssv, pipes. +* Some Kotlin and Java types are fully qualified to avoid conflicts with types defined in OpenAPI definitions. +* Implementation of ApiClient is intended to reduce method counts, specifically to benefit Android targets. + + +## Documentation for API Endpoints + +All URIs are relative to *http://localhost:3000* + +| Class | Method | HTTP request | Description | +| ------------ | ------------- | ------------- | ------------- | +| *BodyApi* | [**testBodyMultipartFormdataWithJsonPart**](docs/BodyApi.md#testbodymultipartformdatawithjsonpart) | **POST** /body/multipart/formdata/with_json_part | Test multipart with JSON part | + + + +## Documentation for Models + + - [org.openapitools.client.models.FileMetadata](docs/FileMetadata.md) + + + +## Documentation for Authorization + +Endpoints do not require authorization. + diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/build.gradle b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/build.gradle new file mode 100644 index 000000000000..d8e895d8fe96 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/build.gradle @@ -0,0 +1,75 @@ +group 'org.openapitools' +version '1.0.0' + +wrapper { + gradleVersion = '8.14.3' + distributionUrl = "https://services.gradle.org/distributions/gradle-$gradleVersion-all.zip" +} + +buildscript { + ext.kotlin_version = '2.2.20' + ext.spotless_version = "7.2.1" + + repositories { + maven { url "https://repo1.maven.org/maven2" } + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "com.diffplug.spotless:spotless-plugin-gradle:$spotless_version" + } +} + +apply plugin: 'kotlin' +apply plugin: 'maven-publish' +apply plugin: 'com.diffplug.spotless' + +repositories { + maven { url "https://repo1.maven.org/maven2" } +} + +// Use spotless plugin to automatically format code, remove unused import, etc +// To apply changes directly to the file, run `gradlew spotlessApply` +// Ref: https://github.com/diffplug/spotless/tree/main/plugin-gradle +spotless { + // comment out below to run spotless as part of the `check` task + enforceCheck false + + format 'misc', { + // define the files (e.g. '*.gradle', '*.md') to apply `misc` to + target '.gitignore' + + // define the steps to apply to those files + trimTrailingWhitespace() + indentWithSpaces() // Takes an integer argument if you don't like 4 + endWithNewline() + } + kotlin { + ktfmt() + } +} + +test { + useJUnitPlatform() +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "com.google.code.gson:gson:2.13.2" + implementation "com.squareup.okhttp3:okhttp:5.1.0" + testImplementation "io.kotlintest:kotlintest-runner-junit5:3.4.2" +} + +java { + withSourcesJar() +} + +publishing { + publications { + maven(MavenPublication) { + groupId = 'org.openapitools' + artifactId = 'kotlin-client' + version = '1.0.0' + from components.java + } + } +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/BodyApi.md b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/BodyApi.md new file mode 100644 index 000000000000..4384296afe39 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/BodyApi.md @@ -0,0 +1,57 @@ +# BodyApi + +All URIs are relative to *http://localhost:3000* + +| Method | HTTP request | Description | +| ------------- | ------------- | ------------- | +| [**testBodyMultipartFormdataWithJsonPart**](BodyApi.md#testBodyMultipartFormdataWithJsonPart) | **POST** /body/multipart/formdata/with_json_part | Test multipart with JSON part | + + + +# **testBodyMultipartFormdataWithJsonPart** +> kotlin.String testBodyMultipartFormdataWithJsonPart(metadata, file) + +Test multipart with JSON part + +Test multipart/form-data with a part that has Content-Type application/json + +### Example +```kotlin +// Import classes: +//import org.openapitools.client.infrastructure.* +//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 { + val result : kotlin.String = apiInstance.testBodyMultipartFormdataWithJsonPart(metadata, file) + println(result) +} catch (e: ClientException) { + println("4xx response calling BodyApi#testBodyMultipartFormdataWithJsonPart") + e.printStackTrace() +} catch (e: ServerException) { + println("5xx response calling BodyApi#testBodyMultipartFormdataWithJsonPart") + e.printStackTrace() +} +``` + +### Parameters +| **metadata** | [**FileMetadata**](FileMetadata.md)| | | +| Name | Type | Description | Notes | +| ------------- | ------------- | ------------- | ------------- | +| **file** | **java.io.File**| File to upload | | + +### Return type + +**kotlin.String** + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: multipart/form-data + - **Accept**: text/plain + diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/FileMetadata.md b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/FileMetadata.md new file mode 100644 index 000000000000..115b2dfed517 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/FileMetadata.md @@ -0,0 +1,12 @@ + +# FileMetadata + +## Properties +| Name | Type | Description | Notes | +| ------------ | ------------- | ------------- | ------------- | +| **id** | **kotlin.Long** | | | +| **name** | **kotlin.String** | | | +| **tags** | **kotlin.collections.List<kotlin.String>** | | [optional] | + + + diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradle/wrapper/gradle-wrapper.jar b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000000..2c3521197d7c Binary files /dev/null and b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradle/wrapper/gradle-wrapper.properties b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..7705927e949f --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradlew b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradlew new file mode 100755 index 000000000000..51eb8bb47109 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while +APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path +[ -h "$app_path" ] +do +ls=$( ls -ld "$app_path" ) +link=${ls#*' -> '} +case $link in #( +/*) app_path=$link ;; #( +*) app_path=$APP_HOME$link ;; +esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { +echo "$*" +} >&2 + +die () { +echo +echo "$*" +echo +exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( +CYGWIN* ) cygwin=true ;; #( +Darwin* ) darwin=true ;; #( +MSYS* | MINGW* ) msys=true ;; #( +NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then +if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +# IBM's JDK on AIX uses strange locations for the executables +JAVACMD=$JAVA_HOME/jre/sh/java +else +JAVACMD=$JAVA_HOME/bin/java +fi +if [ ! -x "$JAVACMD" ] ; then +die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi +else +JAVACMD=java +if ! command -v java >/dev/null 2>&1 +then +die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then +case $MAX_FD in #( +max*) +# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +MAX_FD=$( ulimit -H -n ) || +warn "Could not query maximum file descriptor limit" +esac +case $MAX_FD in #( +'' | soft) :;; #( +*) +# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. +# shellcheck disable=SC2039,SC3045 +ulimit -n "$MAX_FD" || +warn "Could not set maximum file descriptor limit to $MAX_FD" +esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then +APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) +CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + +JAVACMD=$( cygpath --unix "$JAVACMD" ) + +# Now convert the arguments - kludge to limit ourselves to /bin/sh +for arg do +if +case $arg in #( +-*) false ;; # don't mess with options #( +/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath +[ -e "$t" ] ;; #( +*) false ;; +esac +then +arg=$( cygpath --path --ignore --mixed "$arg" ) +fi +# Roll the args list around exactly as many times as the number of +# args, so each arg winds up back in the position where it started, but +# possibly modified. +# +# NB: a `for` loop captures its iteration list before it begins, so +# changing the positional parameters here affects neither the number of +# iterations, nor the values presented in `arg`. +shift # remove old arg +set -- "$@" "$arg" # push replacement arg +done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ +"-Dorg.gradle.appname=$APP_BASE_NAME" \ +-classpath "$CLASSPATH" \ +org.gradle.wrapper.GradleWrapperMain \ +"$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then +die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( +printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | +xargs -n1 | +sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | +tr '\n' ' ' +)" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradlew.bat b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradlew.bat new file mode 100644 index 000000000000..9d21a21834d5 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/settings.gradle b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/settings.gradle new file mode 100644 index 000000000000..431927491c34 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'kotlin-client' diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/apis/BodyApi.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/apis/BodyApi.kt new file mode 100644 index 000000000000..6708ce68cee0 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/apis/BodyApi.kt @@ -0,0 +1,129 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport" +) + +package org.openapitools.client.apis + +import java.io.IOException +import okhttp3.Call +import okhttp3.HttpUrl + +import org.openapitools.client.models.FileMetadata + +import com.google.gson.annotations.SerializedName + +import org.openapitools.client.infrastructure.ApiClient +import org.openapitools.client.infrastructure.ApiResponse +import org.openapitools.client.infrastructure.ClientException +import org.openapitools.client.infrastructure.ClientError +import org.openapitools.client.infrastructure.ServerException +import org.openapitools.client.infrastructure.ServerError +import org.openapitools.client.infrastructure.MultiValueMap +import org.openapitools.client.infrastructure.PartConfig +import org.openapitools.client.infrastructure.RequestConfig +import org.openapitools.client.infrastructure.RequestMethod +import org.openapitools.client.infrastructure.ResponseType +import org.openapitools.client.infrastructure.Success +import org.openapitools.client.infrastructure.toMultiValue + +open class BodyApi(basePath: kotlin.String = defaultBasePath, client: Call.Factory = ApiClient.defaultClient) : ApiClient(basePath, client) { + companion object { + @JvmStatic + val defaultBasePath: String by lazy { + System.getProperties().getProperty(ApiClient.baseUrlKey, "http://localhost:3000") + } + } + + /** + * POST /body/multipart/formdata/with_json_part + * Test multipart with JSON part + * Test multipart/form-data with a part that has Content-Type application/json + * @param metadata + * @param file File to upload + * @return kotlin.String + * @throws IllegalStateException If the request is not correctly configured + * @throws IOException Rethrows the OkHttp execute method exception + * @throws UnsupportedOperationException If the API returns an informational or redirection response + * @throws ClientException If the API returns a client error response + * @throws ServerException If the API returns a server error response + */ + @Suppress("UNCHECKED_CAST") + @Throws(IllegalStateException::class, IOException::class, UnsupportedOperationException::class, ClientException::class, ServerException::class) + fun testBodyMultipartFormdataWithJsonPart(metadata: FileMetadata, file: java.io.File) : kotlin.String { + val localVarResponse = testBodyMultipartFormdataWithJsonPartWithHttpInfo(metadata = metadata, file = file) + + return when (localVarResponse.responseType) { + ResponseType.Success -> (localVarResponse as Success<*>).data as kotlin.String + ResponseType.Informational -> throw UnsupportedOperationException("Client does not support Informational responses.") + ResponseType.Redirection -> throw UnsupportedOperationException("Client does not support Redirection responses.") + ResponseType.ClientError -> { + val localVarError = localVarResponse as ClientError<*> + throw ClientException("Client error : ${localVarError.statusCode} ${localVarError.message.orEmpty()}", localVarError.statusCode, localVarResponse) + } + ResponseType.ServerError -> { + val localVarError = localVarResponse as ServerError<*> + throw ServerException("Server error : ${localVarError.statusCode} ${localVarError.message.orEmpty()} ${localVarError.body}", localVarError.statusCode, localVarResponse) + } + } + } + + /** + * POST /body/multipart/formdata/with_json_part + * Test multipart with JSON part + * Test multipart/form-data with a part that has Content-Type application/json + * @param metadata + * @param file File to upload + * @return ApiResponse + * @throws IllegalStateException If the request is not correctly configured + * @throws IOException Rethrows the OkHttp execute method exception + */ + @Suppress("UNCHECKED_CAST") + @Throws(IllegalStateException::class, IOException::class) + fun testBodyMultipartFormdataWithJsonPartWithHttpInfo(metadata: FileMetadata, file: java.io.File) : ApiResponse { + val localVariableConfig = testBodyMultipartFormdataWithJsonPartRequestConfig(metadata = metadata, file = file) + + return request>, kotlin.String>( + localVariableConfig + ) + } + + /** + * To obtain the request config of the operation testBodyMultipartFormdataWithJsonPart + * + * @param metadata + * @param file File to upload + * @return RequestConfig + */ + fun testBodyMultipartFormdataWithJsonPartRequestConfig(metadata: FileMetadata, file: java.io.File) : RequestConfig>> { + val localVariableBody = mapOf( + "metadata" to PartConfig(body = metadata, headers = mutableMapOf("Content-Type" to "application/json")), + "file" to PartConfig(body = file, headers = mutableMapOf("Content-Type" to "image/jpeg")),) + val localVariableQuery: MultiValueMap = mutableMapOf() + val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") + localVariableHeaders["Accept"] = "text/plain" + + return RequestConfig( + method = RequestMethod.POST, + path = "/body/multipart/formdata/with_json_part", + query = localVariableQuery, + headers = localVariableHeaders, + requiresAuthentication = false, + body = localVariableBody + ) + } + + + private fun encodeURIComponent(uriComponent: kotlin.String): kotlin.String = + HttpUrl.Builder().scheme("http").host("localhost").addPathSegment(uriComponent).build().encodedPathSegments[0] +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiAbstractions.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiAbstractions.kt new file mode 100644 index 000000000000..7fe8da468374 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiAbstractions.kt @@ -0,0 +1,23 @@ +package org.openapitools.client.infrastructure + +typealias MultiValueMap = MutableMap> + +fun collectionDelimiter(collectionFormat: String): String = when(collectionFormat) { + "csv" -> "," + "tsv" -> "\t" + "pipe" -> "|" + "space" -> " " + else -> "" +} + +val defaultMultiValueConverter: (item: Any?) -> String = { item -> "$item" } + +fun toMultiValue(items: Array, collectionFormat: String, map: (item: T) -> String = defaultMultiValueConverter): List + = toMultiValue(items.asIterable(), collectionFormat, map) + +fun toMultiValue(items: Iterable, collectionFormat: String, map: (item: T) -> String = defaultMultiValueConverter): List { + return when(collectionFormat) { + "multi" -> items.map(map) + else -> listOf(items.joinToString(separator = collectionDelimiter(collectionFormat), transform = map)) + } +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt new file mode 100644 index 000000000000..e32a637b7496 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -0,0 +1,371 @@ +package org.openapitools.client.infrastructure + +import okhttp3.OkHttpClient +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.FormBody +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.ResponseBody +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.Headers +import okhttp3.Headers.Builder +import okhttp3.Headers.Companion.toHeaders +import okhttp3.MultipartBody +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Response +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.net.URLConnection +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.OffsetDateTime +import java.time.OffsetTime +import java.util.Locale +import java.util.regex.Pattern +import com.google.gson.reflect.TypeToken + +val EMPTY_REQUEST: RequestBody = ByteArray(0).toRequestBody() + +open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClient) { + companion object { + protected const val ContentType: String = "Content-Type" + protected const val Accept: String = "Accept" + protected const val Authorization: String = "Authorization" + protected const val JsonMediaType: String = "application/json" + protected const val FormDataMediaType: String = "multipart/form-data" + protected const val FormUrlEncMediaType: String = "application/x-www-form-urlencoded" + protected const val XmlMediaType: String = "application/xml" + protected const val OctetMediaType: String = "application/octet-stream" + protected const val TextMediaType: String = "text/plain" + + val apiKey: MutableMap = mutableMapOf() + val apiKeyPrefix: MutableMap = mutableMapOf() + var username: String? = null + var password: String? = null + var accessToken: String? = null + const val baseUrlKey: String = "org.openapitools.client.baseUrl" + + @JvmStatic + val defaultClient: OkHttpClient by lazy { + builder.build() + } + + @JvmStatic + val builder: OkHttpClient.Builder = OkHttpClient.Builder() + } + + /** + * Guess Content-Type header from the given byteArray (defaults to "application/octet-stream"). + * + * @param byteArray The given file + * @return The guessed Content-Type + */ + protected fun guessContentTypeFromByteArray(byteArray: ByteArray): String { + val contentType = try { + URLConnection.guessContentTypeFromStream(byteArray.inputStream()) + } catch (io: IOException) { + "application/octet-stream" + } + return contentType + } + + /** + * Guess Content-Type header from the given file (defaults to "application/octet-stream"). + * + * @param file The given file + * @return The guessed Content-Type + */ + protected fun guessContentTypeFromFile(file: File): String { + val contentType = URLConnection.guessContentTypeFromName(file.name) + return contentType ?: "application/octet-stream" + } + + /** + * Adds a File to a MultipartBody.Builder + * Defined a helper in the requestBody method to not duplicate code + * It will be used when the content is a FormDataMediaType and the body of the PartConfig is a File + * + * @param name The field name to add in the request + * @param headers The headers that are in the PartConfig + * @param file The file that will be added as the field value + * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on + * @see requestBody + */ + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { + // 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() + addPart( + partHeaders.toHeaders(), + file.asRequestBody(fileMediaType) + ) + } + + /** + * Adds any type to a MultipartBody.Builder + * Defined a helper in the requestBody method to not duplicate code + * It will be used when the content is a FormDataMediaType and the body of the PartConfig is not a File. + * + * @param name The field name to add in the request + * @param headers The headers that are in the PartConfig + * @param obj The field name to add in the request + * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on + * @see requestBody + */ + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + // Filter out Content-Type from headers as OkHttp requires it to be passed + // separately via toRequestBody(mediaType), not in the headers map + val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() + + ("Content-Disposition" to "form-data; name=\"$name\"") + val partBody = if (partContentType?.contains("json") == true) { + Serializer.gson.toJson(obj) + } else { + parameterToString(obj) + } + addPart( + partHeaders.toHeaders(), + partBody.toRequestBody(partMediaType) + ) + } + + protected inline fun requestBody(content: T, mediaType: String?): RequestBody = + when { + content is ByteArray -> content.toRequestBody((mediaType ?: guessContentTypeFromByteArray(content)).toMediaTypeOrNull()) + content is File -> content.asRequestBody((mediaType ?: guessContentTypeFromFile(content)).toMediaTypeOrNull()) + mediaType == FormDataMediaType -> + MultipartBody.Builder() + .setType(MultipartBody.FORM) + .apply { + // content's type *must* be Map> + @Suppress("UNCHECKED_CAST") + (content as Map>).forEach { (name, part) -> + when (part.body) { + is File -> addPartToMultiPart(name, part.headers, part.body) + is List<*> -> { + part.body.forEach { + if (it is File) { + addPartToMultiPart(name, part.headers, it) + } else { + addPartToMultiPart(name, part.headers, it) + } + } + } + else -> addPartToMultiPart(name, part.headers, part.body) + } + } + }.build() + mediaType == FormUrlEncMediaType -> { + FormBody.Builder().apply { + // content's type *must* be Map> + @Suppress("UNCHECKED_CAST") + (content as Map>).forEach { (name, part) -> + add(name, parameterToString(part.body)) + } + }.build() + } + mediaType == null || mediaType.startsWith("application/") && mediaType.endsWith("json") -> + if (content == null) { + EMPTY_REQUEST + } else { + Serializer.gson.toJson(content, T::class.java) + .toRequestBody((mediaType ?: JsonMediaType).toMediaTypeOrNull()) + } + mediaType == XmlMediaType -> throw UnsupportedOperationException("xml not currently supported.") + mediaType == TextMediaType && content is String -> + content.toRequestBody(TextMediaType.toMediaTypeOrNull()) + // TODO: this should be extended with other serializers + else -> throw UnsupportedOperationException("requestBody currently only supports JSON body, text body, byte body and File body.") + } + + protected inline fun responseBody(response: Response, mediaType: String? = JsonMediaType): T? { + val body = response.body + if(body == null) { + return null + } else if (T::class.java == Unit::class.java) { + // No need to parse the body when we're not interested in the body + // Useful when API is returning other Content-Type + return null + } else if (T::class.java == File::class.java) { + // return tempFile + val contentDisposition = response.header("Content-Disposition") + + val fileName = if (contentDisposition != null) { + // Get filename from the Content-Disposition header. + val pattern = Pattern.compile("filename=['\"]?([^'\"\\s]+)['\"]?") + val matcher = pattern.matcher(contentDisposition) + if (matcher.find()) { + matcher.group(1) + ?.replace(".*[/\\\\]", "") + ?.replace(";", "") + } else { + null + } + } else { + null + } + + var prefix: String? + val suffix: String? + if (fileName == null) { + prefix = "download" + suffix = "" + } else { + val pos = fileName.lastIndexOf(".") + if (pos == -1) { + prefix = fileName + suffix = null + } else { + prefix = fileName.substring(0, pos) + suffix = fileName.substring(pos) + } + // Files.createTempFile requires the prefix to be at least three characters long + if (prefix.length < 3) { + prefix = "download" + } + } + + // Attention: if you are developing an android app that supports API Level 25 and below, please check flag supportAndroidApiLevel25AndBelow in https://openapi-generator.tech/docs/generators/kotlin#config-options + val tempFile = java.nio.file.Files.createTempFile(prefix, suffix).toFile() + tempFile.deleteOnExit() + body.byteStream().use { inputStream -> + tempFile.outputStream().use { tempFileOutputStream -> + inputStream.copyTo(tempFileOutputStream) + } + } + return tempFile as T + } + + return when { + mediaType == null || (mediaType.startsWith("application/") && mediaType.endsWith("json")) -> { + val bodyContent = body.string() + if (bodyContent.isEmpty()) { + return null + } + Serializer.gson.fromJson(bodyContent, (object: TypeToken(){}).getType()) + } + mediaType == OctetMediaType -> body.bytes() as? T + mediaType == TextMediaType -> body.string() as? T + else -> throw UnsupportedOperationException("responseBody currently only supports JSON body, text body and byte body.") + } + } + + + protected inline fun request(requestConfig: RequestConfig): ApiResponse { + val httpUrl = baseUrl.toHttpUrlOrNull() ?: throw IllegalStateException("baseUrl is invalid.") + + val url = httpUrl.newBuilder() + .addEncodedPathSegments(requestConfig.path.trimStart('/')) + .apply { + requestConfig.query.forEach { query -> + query.value.forEach { queryValue -> + addQueryParameter(query.key, queryValue) + } + } + }.build() + + // take content-type/accept from spec or set to default (application/json) if not defined + if (requestConfig.body != null && requestConfig.headers[ContentType].isNullOrEmpty()) { + requestConfig.headers[ContentType] = JsonMediaType + } + if (requestConfig.headers[Accept].isNullOrEmpty()) { + requestConfig.headers[Accept] = JsonMediaType + } + val headers = requestConfig.headers + + if (headers[Accept].isNullOrEmpty()) { + throw kotlin.IllegalStateException("Missing Accept header. This is required.") + } + + val contentType = if (headers[ContentType] != null) { + // TODO: support multiple contentType options here. + (headers[ContentType] as String).substringBefore(";").lowercase(Locale.US) + } else { + null + } + + val request = when (requestConfig.method) { + RequestMethod.DELETE -> Request.Builder().url(url).delete(requestBody(requestConfig.body, contentType)) + RequestMethod.GET -> Request.Builder().url(url) + RequestMethod.HEAD -> Request.Builder().url(url).head() + RequestMethod.PATCH -> Request.Builder().url(url).patch(requestBody(requestConfig.body, contentType)) + RequestMethod.PUT -> Request.Builder().url(url).put(requestBody(requestConfig.body, contentType)) + RequestMethod.POST -> Request.Builder().url(url).post(requestBody(requestConfig.body, contentType)) + RequestMethod.OPTIONS -> Request.Builder().url(url).method("OPTIONS", null) + }.apply { + val headersBuilder = Headers.Builder() + headers.forEach { header -> + headersBuilder.add(header.key, header.value) + } + this.headers(headersBuilder.build()) + }.build() + + val response = client.newCall(request).execute() + + val accept = response.header(ContentType)?.substringBefore(";")?.lowercase(Locale.US) + + // TODO: handle specific mapping types. e.g. Map> + @Suppress("UNNECESSARY_SAFE_CALL") + return response.use { + when { + it.isRedirect -> Redirection( + it.code, + it.headers.toMultimap() + ) + it.isInformational -> Informational( + it.message, + it.code, + it.headers.toMultimap() + ) + it.isSuccessful -> Success( + responseBody(it, accept), + it.code, + it.headers.toMultimap() + ) + it.isClientError -> ClientError( + it.message, + it.body?.string(), + it.code, + it.headers.toMultimap() + ) + else -> ServerError( + it.message, + it.body?.string(), + it.code, + it.headers.toMultimap() + ) + } + } + } + + protected fun parameterToString(value: Any?): String = when (value) { + null -> "" + is Array<*> -> toMultiValue(value, "csv").toString() + is Iterable<*> -> toMultiValue(value, "csv").toString() + is OffsetDateTime -> parseDateToQueryString(value) + is OffsetTime -> parseDateToQueryString(value) + is LocalDateTime -> parseDateToQueryString(value) + is LocalDate -> parseDateToQueryString(value) + is LocalTime -> parseDateToQueryString(value) + else -> value.toString() + } + + protected inline fun parseDateToQueryString(value : T): String { + /* + .replace("\"", "") converts the json object string to an actual string for the query parameter. + The moshi or gson adapter allows a more generic solution instead of trying to use a native + formatter. It also easily allows to provide a simple way to define a custom date format pattern + inside a gson/moshi adapter. + */ + return Serializer.gson.toJson(value, T::class.java).replace("\"", "") + } +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiResponse.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiResponse.kt new file mode 100644 index 000000000000..689fb03cd7ae --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ApiResponse.kt @@ -0,0 +1,43 @@ +package org.openapitools.client.infrastructure + +enum class ResponseType { + Success, Informational, Redirection, ClientError, ServerError +} + +interface Response + +abstract class ApiResponse(val responseType: ResponseType): Response { + abstract val statusCode: Int + abstract val headers: Map> +} + +class Success( + val data: T, + override val statusCode: Int = -1, + override val headers: Map> = mapOf() +): ApiResponse(ResponseType.Success) + +class Informational( + val statusText: String, + override val statusCode: Int = -1, + override val headers: Map> = mapOf() +) : ApiResponse(ResponseType.Informational) + +class Redirection( + override val statusCode: Int = -1, + override val headers: Map> = mapOf() +) : ApiResponse(ResponseType.Redirection) + +class ClientError( + val message: String? = null, + val body: Any? = null, + override val statusCode: Int = -1, + override val headers: Map> = mapOf() +) : ApiResponse(ResponseType.ClientError) + +class ServerError( + val message: String? = null, + val body: Any? = null, + override val statusCode: Int = -1, + override val headers: Map> = mapOf() +): ApiResponse(ResponseType.ServerError) diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ByteArrayAdapter.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ByteArrayAdapter.kt new file mode 100644 index 000000000000..6120b081929d --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ByteArrayAdapter.kt @@ -0,0 +1,33 @@ +package org.openapitools.client.infrastructure + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import com.google.gson.stream.JsonToken.NULL +import java.io.IOException + +class ByteArrayAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter?, value: ByteArray?) { + if (value == null) { + out?.nullValue() + } else { + out?.value(String(value)) + } + } + + @Throws(IOException::class) + override fun read(out: JsonReader?): ByteArray? { + out ?: return null + + when (out.peek()) { + NULL -> { + out.nextNull() + return null + } + else -> { + return out.nextString().toByteArray() + } + } + } +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/Errors.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/Errors.kt new file mode 100644 index 000000000000..c83993b9055c --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/Errors.kt @@ -0,0 +1,18 @@ +@file:Suppress("unused") +package org.openapitools.client.infrastructure + +import java.lang.RuntimeException + +open class ClientException(message: kotlin.String? = null, val statusCode: Int = -1, val response: Response? = null) : RuntimeException(message) { + + companion object { + private const val serialVersionUID: Long = 123L + } +} + +open class ServerException(message: kotlin.String? = null, val statusCode: Int = -1, val response: Response? = null) : RuntimeException(message) { + + companion object { + private const val serialVersionUID: Long = 456L + } +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/LocalDateAdapter.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/LocalDateAdapter.kt new file mode 100644 index 000000000000..30ef6697183a --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/LocalDateAdapter.kt @@ -0,0 +1,35 @@ +package org.openapitools.client.infrastructure + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import com.google.gson.stream.JsonToken.NULL +import java.io.IOException +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +class LocalDateAdapter(private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE) : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter?, value: LocalDate?) { + if (value == null) { + out?.nullValue() + } else { + out?.value(formatter.format(value)) + } + } + + @Throws(IOException::class) + override fun read(out: JsonReader?): LocalDate? { + out ?: return null + + when (out.peek()) { + NULL -> { + out.nextNull() + return null + } + else -> { + return LocalDate.parse(out.nextString(), formatter) + } + } + } +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/LocalDateTimeAdapter.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/LocalDateTimeAdapter.kt new file mode 100644 index 000000000000..3ad781c66ca1 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/LocalDateTimeAdapter.kt @@ -0,0 +1,35 @@ +package org.openapitools.client.infrastructure + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import com.google.gson.stream.JsonToken.NULL +import java.io.IOException +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +class LocalDateTimeAdapter(private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME) : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter?, value: LocalDateTime?) { + if (value == null) { + out?.nullValue() + } else { + out?.value(formatter.format(value)) + } + } + + @Throws(IOException::class) + override fun read(out: JsonReader?): LocalDateTime? { + out ?: return null + + when (out.peek()) { + NULL -> { + out.nextNull() + return null + } + else -> { + return LocalDateTime.parse(out.nextString(), formatter) + } + } + } +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/OffsetDateTimeAdapter.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/OffsetDateTimeAdapter.kt new file mode 100644 index 000000000000..e615135c9cc0 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/OffsetDateTimeAdapter.kt @@ -0,0 +1,35 @@ +package org.openapitools.client.infrastructure + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import com.google.gson.stream.JsonToken.NULL +import java.io.IOException +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter + +class OffsetDateTimeAdapter(private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME) : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter?, value: OffsetDateTime?) { + if (value == null) { + out?.nullValue() + } else { + out?.value(formatter.format(value)) + } + } + + @Throws(IOException::class) + override fun read(out: JsonReader?): OffsetDateTime? { + out ?: return null + + when (out.peek()) { + NULL -> { + out.nextNull() + return null + } + else -> { + return OffsetDateTime.parse(out.nextString(), formatter) + } + } + } +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt new file mode 100644 index 000000000000..be00e38fbaee --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -0,0 +1,11 @@ +package org.openapitools.client.infrastructure + +/** + * Defines a config object for a given part of a multi-part request. + * NOTE: Headers is a Map because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class PartConfig( + val headers: MutableMap = mutableMapOf(), + val body: T? = null +) diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt new file mode 100644 index 000000000000..6578b9381b78 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/RequestConfig.kt @@ -0,0 +1,19 @@ +package org.openapitools.client.infrastructure + +/** + * Defines a config object for a given request. + * NOTE: This object doesn't include 'body' because it + * allows for caching of the constructed object + * for many request definitions. + * NOTE: Headers is a Map because rfc2616 defines + * multi-valued headers as csv-only. + */ +data class RequestConfig( + val method: RequestMethod, + val path: String, + val headers: MutableMap = mutableMapOf(), + val params: MutableMap = mutableMapOf(), + val query: MutableMap> = mutableMapOf(), + val requiresAuthentication: Boolean, + val body: T? = null +) diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/RequestMethod.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/RequestMethod.kt new file mode 100644 index 000000000000..beb56f07cdde --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/RequestMethod.kt @@ -0,0 +1,8 @@ +package org.openapitools.client.infrastructure + +/** + * Provides enumerated HTTP verbs + */ +enum class RequestMethod { + GET, DELETE, HEAD, OPTIONS, PATCH, POST, PUT +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ResponseExtensions.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ResponseExtensions.kt new file mode 100644 index 000000000000..9bd2790dc144 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/ResponseExtensions.kt @@ -0,0 +1,24 @@ +package org.openapitools.client.infrastructure + +import okhttp3.Response + +/** + * Provides an extension to evaluation whether the response is a 1xx code + */ +val Response.isInformational : Boolean get() = this.code in 100..199 + +/** + * Provides an extension to evaluation whether the response is a 3xx code + */ +@Suppress("EXTENSION_SHADOWED_BY_MEMBER") +val Response.isRedirect : Boolean get() = this.code in 300..399 + +/** + * Provides an extension to evaluation whether the response is a 4xx code + */ +val Response.isClientError : Boolean get() = this.code in 400..499 + +/** + * Provides an extension to evaluation whether the response is a 5xx (Standard) through 999 (non-standard) code + */ +val Response.isServerError : Boolean get() = this.code in 500..999 diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/Serializer.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/Serializer.kt new file mode 100644 index 000000000000..6e16e4f6582b --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/infrastructure/Serializer.kt @@ -0,0 +1,22 @@ +package org.openapitools.client.infrastructure + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.util.UUID + +object Serializer { + @JvmStatic + val gsonBuilder: GsonBuilder = GsonBuilder() + .registerTypeAdapter(OffsetDateTime::class.java, OffsetDateTimeAdapter()) + .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeAdapter()) + .registerTypeAdapter(LocalDate::class.java, LocalDateAdapter()) + .registerTypeAdapter(ByteArray::class.java, ByteArrayAdapter()) + + @JvmStatic + val gson: Gson by lazy { + gsonBuilder.create() + } +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/models/FileMetadata.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/models/FileMetadata.kt new file mode 100644 index 000000000000..13a4f8af4821 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/main/kotlin/org/openapitools/client/models/FileMetadata.kt @@ -0,0 +1,45 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport" +) + +package org.openapitools.client.models + + +import com.google.gson.annotations.SerializedName + +/** + * + * + * @param id + * @param name + * @param tags + */ + + +data class FileMetadata ( + + @SerializedName("id") + val id: kotlin.Long, + + @SerializedName("name") + val name: kotlin.String, + + @SerializedName("tags") + val tags: kotlin.collections.List? = null + +) { + + +} + diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/EchoServerResponseParser.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/EchoServerResponseParser.kt new file mode 100644 index 000000000000..fe0981d0e01f --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/EchoServerResponseParser.kt @@ -0,0 +1,44 @@ +package org.openapitools.client + +class EchoServerResponseParser(response: String) { + lateinit var method: String + lateinit var path: String + lateinit var protocol: String + val headers = hashMapOf() + val body: String + + init { + require(response.isNotEmpty()) { "Echo server response cannot be null" } + + val lines = response.lineSequence().iterator() + var firstLine = true + var bodyStart = false + val bodyBuilder = StringBuilder() + for (line in lines) { + if (firstLine) { + val items = line.split(" ") + method = items[0] + path = items[1] + protocol = items[2] + firstLine = false + continue + } + if (bodyStart) { + bodyBuilder.append(line) + bodyBuilder.append("\n") + } + if (line.isEmpty()) { + bodyStart = true + continue + } + + val keyValue = line.split(": ") + if (keyValue.size == 2) { + headers[keyValue[0]] = keyValue[1] + } + } + body = bodyBuilder.toString().trimEnd() + } +} + + diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/MultipartJsonTest.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/MultipartJsonTest.kt new file mode 100644 index 000000000000..5d42a6f6f4b5 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/MultipartJsonTest.kt @@ -0,0 +1,69 @@ +package org.openapitools.client + +import io.kotlintest.matchers.string.shouldContain +import io.kotlintest.matchers.string.shouldNotContain +import io.kotlintest.specs.ShouldSpec +import org.junit.jupiter.api.Assertions +import org.openapitools.client.apis.BodyApi +import org.openapitools.client.models.FileMetadata +import java.io.File +import java.io.FileWriter + +class MultipartJsonTest : ShouldSpec({ + val bodyApi = BodyApi() + + should("serialize JSON part with proper JSON format, not toString()") { + val metadata = FileMetadata( + id = 12345L, + name = "test-file", + tags = listOf("tag1", "tag2") + ) + val testFile = createTestFile() + + val result = bodyApi.testBodyMultipartFormdataWithJsonPart(metadata, testFile) + val parsedResult = EchoServerResponseParser(result) + + // The metadata part should contain proper JSON serialization + parsedResult.body shouldContain """"id":12345""" + parsedResult.body shouldContain """"name":"test-file"""" + parsedResult.body shouldContain """"tags":["tag1","tag2"]""" + + // Should NOT contain Kotlin's toString() format like "FileMetadata(id=12345, name=test-file)" + parsedResult.body shouldNotContain "FileMetadata(" + + // Should have proper Content-Type for metadata part + parsedResult.body shouldContain """Content-Type: application/json""" + + // File part should be present + parsedResult.body shouldContain """Content-Disposition: form-data; name="file"; filename="test.txt"""" + parsedResult.body shouldContain """testing multipart with json""" + } + + should("not throw IllegalArgumentException for Content-Type in headers") { + // This test verifies that Content-Type is properly filtered from headers + // and passed to OkHttp's asRequestBody/toRequestBody methods instead + val metadata = FileMetadata( + id = 999L, + name = "another-test" + ) + val testFile = createTestFile() + + // Should not throw: java.lang.IllegalArgumentException: Unexpected header: Content-Type + val result = bodyApi.testBodyMultipartFormdataWithJsonPart(metadata, testFile) + + // If we get here without exception, the fix is working + Assertions.assertNotNull(result) + } +}) + +private fun createTestFile(): File { + val myFile = File("test.txt") + if (!myFile.exists()) { + Assertions.assertTrue(myFile.createNewFile()) + } + val fw = FileWriter(myFile) + fw.write("testing multipart with json") + fw.close() + myFile.deleteOnExit() + return myFile +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/apis/BodyApiTest.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/apis/BodyApiTest.kt new file mode 100644 index 000000000000..ddcb787b4322 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/apis/BodyApiTest.kt @@ -0,0 +1,39 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport" +) + +package org.openapitools.client.apis + +import io.kotlintest.shouldBe +import io.kotlintest.specs.ShouldSpec + +import org.openapitools.client.apis.BodyApi +import org.openapitools.client.models.FileMetadata + +class BodyApiTest : ShouldSpec() { + init { + // uncomment below to create an instance of BodyApi + //val apiInstance = BodyApi() + + // to test testBodyMultipartFormdataWithJsonPart + should("test testBodyMultipartFormdataWithJsonPart") { + // uncomment below to test testBodyMultipartFormdataWithJsonPart + //val metadata : FileMetadata = // FileMetadata | + //val file : java.io.File = BINARY_DATA_HERE // java.io.File | File to upload + //val result : kotlin.String = apiInstance.testBodyMultipartFormdataWithJsonPart(metadata, file) + //result shouldBe ("TODO") + } + + } +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/models/FileMetadataTest.kt b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/models/FileMetadataTest.kt new file mode 100644 index 000000000000..e441b43c8800 --- /dev/null +++ b/samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/src/test/kotlin/org/openapitools/client/models/FileMetadataTest.kt @@ -0,0 +1,47 @@ +/** + * + * Please note: + * This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit this file manually. + * + */ + +@file:Suppress( + "ArrayInDataClass", + "EnumEntryName", + "RemoveRedundantQualifierName", + "UnusedImport" +) + +package org.openapitools.client.models + +import io.kotlintest.shouldBe +import io.kotlintest.specs.ShouldSpec + +import org.openapitools.client.models.FileMetadata + +class FileMetadataTest : ShouldSpec() { + init { + // uncomment below to create an instance of FileMetadata + //val modelInstance = FileMetadata() + + // to test the property `id` + should("test id") { + // uncomment below to test the property + //modelInstance.id shouldBe ("TODO") + } + + // to test the property `name` + should("test name") { + // uncomment below to test the property + //modelInstance.name shouldBe ("TODO") + } + + // to test the property `tags` + should("test tags") { + // uncomment below to test the property + //modelInstance.tags shouldBe ("TODO") + } + + } +} diff --git a/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index c681bff3ed9e..53456dc6d566 100644 --- a/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/echo_api/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/echo_api/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/echo_api/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/echo_api/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/echo_api/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/echo_api/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/echo_api/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/echo_api/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 14546d820360..608a208cac4a 100644 --- a/samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/others/kotlin-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/others/kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/others/kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 7b323e0bb54c..2ddfd72c32c2 100644 --- a/samples/client/others/kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/others/kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/others/kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/others/kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/others/kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/others/kotlin-jvm-okhttp-non-ascii-headers/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/others/kotlin-jvm-okhttp-parameter-tests/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/others/kotlin-jvm-okhttp-parameter-tests/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 14546d820360..608a208cac4a 100644 --- a/samples/client/others/kotlin-jvm-okhttp-parameter-tests/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/others/kotlin-jvm-okhttp-parameter-tests/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/others/kotlin-jvm-okhttp-parameter-tests/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/others/kotlin-jvm-okhttp-parameter-tests/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/others/kotlin-jvm-okhttp-parameter-tests/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/others/kotlin-jvm-okhttp-parameter-tests/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/others/kotlin-jvm-okhttp-path-comments/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/others/kotlin-jvm-okhttp-path-comments/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index d8aff9820512..3b5b923b5a27 100644 --- a/samples/client/others/kotlin-jvm-okhttp-path-comments/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/others/kotlin-jvm-okhttp-path-comments/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/others/kotlin-jvm-okhttp-path-comments/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/others/kotlin-jvm-okhttp-path-comments/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/others/kotlin-jvm-okhttp-path-comments/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/others/kotlin-jvm-okhttp-path-comments/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/README.md b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/README.md index 9e7c9dfd8d50..e8adfecabfc3 100644 --- a/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/README.md +++ b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/README.md @@ -47,6 +47,7 @@ All URIs are relative to *http://example.org* | Class | Method | HTTP request | Description | | ------------ | ------------- | ------------- | ------------- | | *BirdApi* | [**getBird**](docs/BirdApi.md#getbird) | **GET** /v1/bird/{id} | | +| *BirdApi* | [**uploadBirdWithMetadata**](docs/BirdApi.md#uploadbirdwithmetadata) | **POST** /v1/bird/upload | | diff --git a/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/docs/BirdApi.md b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/docs/BirdApi.md index 60283e9b7cd3..4422300d940d 100644 --- a/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/docs/BirdApi.md +++ b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/docs/BirdApi.md @@ -5,6 +5,7 @@ All URIs are relative to *http://example.org* | Method | HTTP request | Description | | ------------- | ------------- | ------------- | | [**getBird**](BirdApi.md#getBird) | **GET** /v1/bird/{id} | | +| [**uploadBirdWithMetadata**](BirdApi.md#uploadBirdWithMetadata) | **POST** /v1/bird/upload | | @@ -51,3 +52,49 @@ No authorization required - **Content-Type**: Not defined - **Accept**: application/json + +# **uploadBirdWithMetadata** +> kotlin.String uploadBirdWithMetadata(metadata, file) + + + +### Example +```kotlin +// Import classes: +//import org.openapitools.client.infrastructure.* +//import org.openapitools.client.models.* + +val apiInstance = BirdApi() +val metadata : Bird = // Bird | +val file : java.io.File = BINARY_DATA_HERE // java.io.File | +try { + val result : kotlin.String = apiInstance.uploadBirdWithMetadata(metadata, file) + println(result) +} catch (e: ClientException) { + println("4xx response calling BirdApi#uploadBirdWithMetadata") + e.printStackTrace() +} catch (e: ServerException) { + println("5xx response calling BirdApi#uploadBirdWithMetadata") + e.printStackTrace() +} +``` + +### Parameters +| **metadata** | [**Bird**](Bird.md)| | | +| Name | Type | Description | Notes | +| ------------- | ------------- | ------------- | ------------- | +| **file** | **java.io.File**| | | + +### Return type + +**kotlin.String** + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: multipart/form-data + - **Accept**: text/plain + diff --git a/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/apis/BirdApi.kt b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/apis/BirdApi.kt index 9d2a51531fe2..b12852467001 100644 --- a/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/apis/BirdApi.kt +++ b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/apis/BirdApi.kt @@ -37,6 +37,7 @@ import org.openapitools.client.infrastructure.RequestMethod import org.openapitools.client.infrastructure.ResponseType import org.openapitools.client.infrastructure.Success import org.openapitools.client.infrastructure.toMultiValue +import org.openapitools.client.infrastructure.Serializer open class BirdApi(basePath: kotlin.String = defaultBasePath, client: Call.Factory = ApiClient.defaultClient) : ApiClient(basePath, client) { companion object { @@ -119,6 +120,84 @@ open class BirdApi(basePath: kotlin.String = defaultBasePath, client: Call.Facto ) } + /** + * POST /v1/bird/upload + * + * + * @param metadata + * @param file + * @return kotlin.String + * @throws IllegalStateException If the request is not correctly configured + * @throws IOException Rethrows the OkHttp execute method exception + * @throws UnsupportedOperationException If the API returns an informational or redirection response + * @throws ClientException If the API returns a client error response + * @throws ServerException If the API returns a server error response + */ + @Suppress("UNCHECKED_CAST") + @Throws(IllegalStateException::class, IOException::class, UnsupportedOperationException::class, ClientException::class, ServerException::class) + fun uploadBirdWithMetadata(metadata: Bird, file: java.io.File) : kotlin.String { + val localVarResponse = uploadBirdWithMetadataWithHttpInfo(metadata = metadata, file = file) + + return when (localVarResponse.responseType) { + ResponseType.Success -> (localVarResponse as Success<*>).data as kotlin.String + ResponseType.Informational -> throw UnsupportedOperationException("Client does not support Informational responses.") + ResponseType.Redirection -> throw UnsupportedOperationException("Client does not support Redirection responses.") + ResponseType.ClientError -> { + val localVarError = localVarResponse as ClientError<*> + throw ClientException("Client error : ${localVarError.statusCode} ${localVarError.message.orEmpty()}", localVarError.statusCode, localVarResponse) + } + ResponseType.ServerError -> { + val localVarError = localVarResponse as ServerError<*> + throw ServerException("Server error : ${localVarError.statusCode} ${localVarError.message.orEmpty()} ${localVarError.body}", localVarError.statusCode, localVarResponse) + } + } + } + + /** + * POST /v1/bird/upload + * + * + * @param metadata + * @param file + * @return ApiResponse + * @throws IllegalStateException If the request is not correctly configured + * @throws IOException Rethrows the OkHttp execute method exception + */ + @Suppress("UNCHECKED_CAST") + @Throws(IllegalStateException::class, IOException::class) + fun uploadBirdWithMetadataWithHttpInfo(metadata: Bird, file: java.io.File) : ApiResponse { + val localVariableConfig = uploadBirdWithMetadataRequestConfig(metadata = metadata, file = file) + + return request>, kotlin.String>( + localVariableConfig + ) + } + + /** + * To obtain the request config of the operation uploadBirdWithMetadata + * + * @param metadata + * @param file + * @return RequestConfig + */ + fun uploadBirdWithMetadataRequestConfig(metadata: Bird, file: java.io.File) : RequestConfig>> { + val localVariableBody = mapOf( + "metadata" to PartConfig(body = metadata, headers = mutableMapOf("Content-Type" to "application/json"), serializer = { obj -> Serializer.kotlinxSerializationJson.encodeToString(obj as Bird) }), + "file" to PartConfig(body = file, headers = mutableMapOf()),) + val localVariableQuery: MultiValueMap = mutableMapOf() + val localVariableHeaders: MutableMap = mutableMapOf("Content-Type" to "multipart/form-data") + localVariableHeaders["Accept"] = "text/plain" + + return RequestConfig( + method = RequestMethod.POST, + path = "/v1/bird/upload", + query = localVariableQuery, + headers = localVariableHeaders, + requiresAuthentication = false, + body = localVariableBody + ) + } + private fun encodeURIComponent(uriComponent: kotlin.String): kotlin.String = HttpUrl.Builder().scheme("http").host("localhost").addPathSegment(uriComponent).build().encodedPathSegments[0] diff --git a/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 64f8281e060a..82abe8c6e784 100644 --- a/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -87,6 +87,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -99,15 +118,37 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + // Note: Without a custom serializer, kotlinx.serialization cannot serialize Any? + // The custom serializer should be provided at PartConfig creation to capture type info + parameterToString(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -116,15 +157,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -146,11 +189,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/test/kotlin/org/openapitools/client/infrastructure/MultipartJsonSerializationTest.kt b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/test/kotlin/org/openapitools/client/infrastructure/MultipartJsonSerializationTest.kt new file mode 100644 index 000000000000..fe275fdafc7f --- /dev/null +++ b/samples/client/petstore/kotlin-allOf-discriminator-kotlinx-serialization/src/test/kotlin/org/openapitools/client/infrastructure/MultipartJsonSerializationTest.kt @@ -0,0 +1,34 @@ +package org.openapitools.client.infrastructure + +import io.kotlintest.shouldNotThrow +import io.kotlintest.specs.ShouldSpec +import org.openapitools.client.apis.BirdApi +import org.openapitools.client.models.Bird +import java.io.File +import java.math.BigDecimal +import java.util.UUID + +class MultipartJsonSerializationTest : ShouldSpec({ + should("serialize Bird model in multipart without SerializationException") { + val bird = Bird( + id = UUID.randomUUID(), + featherType = "fluffy", + optionalProperty = BigDecimal("42.0") + ) + + // Create a test file + val testFile = File.createTempFile("test", ".txt") + testFile.writeText("test content") + testFile.deleteOnExit() + + val birdApi = BirdApi("http://example.org") + + // This should NOT throw SerializationException + // The generated code creates a PartConfig with a serializer lambda that captures the Bird type + // Before this fix, kotlinx.serialization would fail with "Cannot serialize Any?" + shouldNotThrow { + // We build the RequestConfig which creates PartConfig with the serializer + birdApi.uploadBirdWithMetadataRequestConfig(metadata = bird, file = testFile) + } + } +}) diff --git a/samples/client/petstore/kotlin-allOf-discriminator/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-allOf-discriminator/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 14546d820360..608a208cac4a 100644 --- a/samples/client/petstore/kotlin-allOf-discriminator/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-allOf-discriminator/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-allOf-discriminator/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-allOf-discriminator/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-allOf-discriminator/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-allOf-discriminator/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-array-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-array-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 14546d820360..608a208cac4a 100644 --- a/samples/client/petstore/kotlin-array-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-array-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-array-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-array-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-array-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-array-integer-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/gradlew b/samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/gradlew old mode 100644 new mode 100755 diff --git a/samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 14546d820360..608a208cac4a 100644 --- a/samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-array-simple-string-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-array-simple-string-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt b/samples/client/petstore/kotlin-array-simple-string-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt index 08ccdb94bbf2..387a5ac8bbb6 100644 --- a/samples/client/petstore/kotlin-array-simple-string-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt +++ b/samples/client/petstore/kotlin-array-simple-string-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt @@ -23,6 +23,10 @@ import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine import kotlinx.serialization.json.Json import io.ktor.http.ParametersBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.ContentType +import io.ktor.http.content.PartData import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* diff --git a/samples/client/petstore/kotlin-array-simple-string-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-array-simple-string-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-array-simple-string-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-array-simple-string-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-bigdecimal-default-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt b/samples/client/petstore/kotlin-bigdecimal-default-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt index 56c0a2aeadcc..efadf7cea19a 100644 --- a/samples/client/petstore/kotlin-bigdecimal-default-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt +++ b/samples/client/petstore/kotlin-bigdecimal-default-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt @@ -24,6 +24,10 @@ import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine import kotlinx.serialization.json.Json import io.ktor.http.ParametersBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.ContentType +import io.ktor.http.content.PartData import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* diff --git a/samples/client/petstore/kotlin-bigdecimal-default-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-bigdecimal-default-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-bigdecimal-default-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-bigdecimal-default-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-bigdecimal-default-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-bigdecimal-default-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 14546d820360..608a208cac4a 100644 --- a/samples/client/petstore/kotlin-bigdecimal-default-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-bigdecimal-default-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-bigdecimal-default-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-bigdecimal-default-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-bigdecimal-default-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-bigdecimal-default-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-default-values-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-default-values-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 14546d820360..608a208cac4a 100644 --- a/samples/client/petstore/kotlin-default-values-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-default-values-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-default-values-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-default-values-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-default-values-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-default-values-jvm-okhttp4/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-default-values-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt b/samples/client/petstore/kotlin-default-values-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt index d27ffacb3d91..255de9e58e83 100644 --- a/samples/client/petstore/kotlin-default-values-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt +++ b/samples/client/petstore/kotlin-default-values-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/DefaultApi.kt @@ -23,6 +23,10 @@ import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine import kotlinx.serialization.json.Json import io.ktor.http.ParametersBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.ContentType +import io.ktor.http.content.PartData import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -81,16 +85,16 @@ open class DefaultApi : ApiClient { val localVariableBody = formData { - fi0?.apply { append("fi0", fi0) } - fi1?.apply { append("fi1", fi1) } - fi2?.apply { append("fi2", fi2) } - fi3?.apply { append("fi3", fi3) } - fn0?.apply { append("fn0", fn0) } - fn1?.apply { append("fn1", fn1) } - fn2?.apply { append("fn2", fn2) } - fn3?.apply { append("fn3", fn3) } + fi0?.apply { append("fi0", fi0.toString()) } + fi1?.apply { append("fi1", fi1.toString()) } + fi2?.apply { append("fi2", fi2.toString()) } + fi3?.apply { append("fi3", fi3.toString()) } + fn0?.apply { append("fn0", fn0.toString()) } + fn1?.apply { append("fn1", fn1.toString()) } + fn2?.apply { append("fn2", fn2.toString()) } + fn3?.apply { append("fn3", fn3.toString()) } fn4?.onEach { - append("fn4", it) + append("fn4", it.toString()) } } diff --git a/samples/client/petstore/kotlin-default-values-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-default-values-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-default-values-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-default-values-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 14546d820360..608a208cac4a 100644 --- a/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-enum-default-value/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index d8ca8529f445..27b2d0a1d647 100644 --- a/samples/client/petstore/kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ public open class ApiClient(public val baseUrl: String, public val client: Call. return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ public open class ApiClient(public val baseUrl: String, public val client: Call. * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ public open class ApiClient(public val baseUrl: String, public val client: Call. * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ public open class ApiClient(public val baseUrl: String, public val client: Call. if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index 474ed67f4002..a9d46bf6c53a 100644 --- a/samples/client/petstore/kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-explicit/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ public data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index b82045dcf7c9..e4d1d2ec2b5b 100644 --- a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.gson.toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-gson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index a9557faaeea0..81368b73d125 100644 --- a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.jacksonObjectMapper.writeValueAsString(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-jackson/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt index 0641a3a45a0b..8a466b82b9bc 100644 --- a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/PetApi.kt @@ -38,6 +38,7 @@ import org.openapitools.client.infrastructure.RequestMethod import org.openapitools.client.infrastructure.ResponseType import org.openapitools.client.infrastructure.Success import org.openapitools.client.infrastructure.toMultiValue +import org.openapitools.client.infrastructure.Serializer open class PetApi(basePath: kotlin.String = defaultBasePath, client: Call.Factory = ApiClient.defaultClient) : ApiClient(basePath, client) { companion object { diff --git a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt index 60177faadaa0..8aac14de6e43 100644 --- a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -37,6 +37,7 @@ import org.openapitools.client.infrastructure.RequestMethod import org.openapitools.client.infrastructure.ResponseType import org.openapitools.client.infrastructure.Success import org.openapitools.client.infrastructure.toMultiValue +import org.openapitools.client.infrastructure.Serializer open class StoreApi(basePath: kotlin.String = defaultBasePath, client: Call.Factory = ApiClient.defaultClient) : ApiClient(basePath, client) { companion object { diff --git a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt index b71f8d39d9bd..9fb061238832 100644 --- a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/apis/UserApi.kt @@ -37,6 +37,7 @@ import org.openapitools.client.infrastructure.RequestMethod import org.openapitools.client.infrastructure.ResponseType import org.openapitools.client.infrastructure.Success import org.openapitools.client.infrastructure.toMultiValue +import org.openapitools.client.infrastructure.Serializer open class UserApi(basePath: kotlin.String = defaultBasePath, client: Call.Factory = ApiClient.defaultClient) : ApiClient(basePath, client) { companion object { diff --git a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index ac1275ddf499..3b8c498e110b 100644 --- a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -88,6 +88,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -100,15 +119,37 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + // Note: Without a custom serializer, kotlinx.serialization cannot serialize Any? + // The custom serializer should be provided at PartConfig creation to capture type info + parameterToString(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -117,15 +158,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -147,11 +190,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-json-request-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 673be9e32774..104be9ffad7c 100644 --- a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -89,6 +89,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -101,15 +120,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.gson.toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -118,15 +157,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -148,11 +189,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-jvm-okhttp4-coroutines/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-jvm-spring-2-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-jvm-spring-2-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-jvm-spring-2-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-jvm-spring-2-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-jvm-spring-3-restclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-jvm-spring-3-webclient/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-kotlinx-datetime/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-kotlinx-datetime/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index b650bfc5f8fc..24ba4d333f1b 100644 --- a/samples/client/petstore/kotlin-kotlinx-datetime/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-kotlinx-datetime/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-kotlinx-datetime/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-kotlinx-datetime/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-kotlinx-datetime/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-kotlinx-datetime/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index b650bfc5f8fc..24ba4d333f1b 100644 --- a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-modelMutable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index b650bfc5f8fc..24ba4d333f1b 100644 --- a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-moshi-codegen/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/README.md b/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/README.md index 0631d896030a..390cabcc4d33 100644 --- a/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/README.md +++ b/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/README.md @@ -38,6 +38,7 @@ All URIs are relative to *http://example.org* | Class | Method | HTTP request | Description | | ------------ | ------------- | ------------- | ------------- | | *BirdApi* | [**getBird**](docs/BirdApi.md#getbird) | **GET** /v1/bird/{id} | | +| *BirdApi* | [**uploadBirdWithMetadata**](docs/BirdApi.md#uploadbirdwithmetadata) | **POST** /v1/bird/upload | | diff --git a/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/docs/BirdApi.md b/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/docs/BirdApi.md index 9eea072053dd..af920447b8c4 100644 --- a/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/docs/BirdApi.md +++ b/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/docs/BirdApi.md @@ -5,6 +5,7 @@ All URIs are relative to *http://example.org* | Method | HTTP request | Description | | ------------- | ------------- | ------------- | | [**getBird**](BirdApi.md#getBird) | **GET** /v1/bird/{id} | | +| [**uploadBirdWithMetadata**](BirdApi.md#uploadBirdWithMetadata) | **POST** /v1/bird/upload | | @@ -51,3 +52,49 @@ No authorization required - **Content-Type**: Not defined - **Accept**: application/json + +# **uploadBirdWithMetadata** +> kotlin.String uploadBirdWithMetadata(metadata, file) + + + +### Example +```kotlin +// Import classes: +//import org.openapitools.client.infrastructure.* +//import org.openapitools.client.models.* + +val apiInstance = BirdApi() +val metadata : Bird = // Bird | +val file : io.ktor.client.request.forms.FormPart = BINARY_DATA_HERE // io.ktor.client.request.forms.FormPart | +try { + val result : kotlin.String = apiInstance.uploadBirdWithMetadata(metadata, file) + println(result) +} catch (e: ClientException) { + println("4xx response calling BirdApi#uploadBirdWithMetadata") + e.printStackTrace() +} catch (e: ServerException) { + println("5xx response calling BirdApi#uploadBirdWithMetadata") + e.printStackTrace() +} +``` + +### Parameters +| **metadata** | [**Bird**](Bird.md)| | | +| Name | Type | Description | Notes | +| ------------- | ------------- | ------------- | ------------- | +| **file** | **io.ktor.client.request.forms.FormPart<io.ktor.client.request.forms.InputProvider>**| | | + +### Return type + +**kotlin.String** + +### Authorization + +No authorization required + +### HTTP request headers + + - **Content-Type**: multipart/form-data + - **Accept**: text/plain + diff --git a/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/src/commonMain/kotlin/org/openapitools/client/apis/BirdApi.kt b/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/src/commonMain/kotlin/org/openapitools/client/apis/BirdApi.kt index c1549d83216d..5ba97f5930a8 100644 --- a/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/src/commonMain/kotlin/org/openapitools/client/apis/BirdApi.kt +++ b/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/src/commonMain/kotlin/org/openapitools/client/apis/BirdApi.kt @@ -24,6 +24,10 @@ import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine import kotlinx.serialization.json.Json import io.ktor.http.ParametersBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.ContentType +import io.ktor.http.content.PartData import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -75,4 +79,41 @@ open class BirdApi : ApiClient { } + /** + * + * + * @param metadata + * @param file + * @return kotlin.String + */ + @Suppress("UNCHECKED_CAST") + open suspend fun uploadBirdWithMetadata(metadata: Bird, file: io.ktor.client.request.forms.FormPart): HttpResponse { + + val localVariableAuthNames = listOf() + + val localVariableBody = + formData { + metadata?.apply { append("metadata", ApiClient.JSON_DEFAULT.encodeToString(Bird.serializer(), metadata)) } + file?.apply { append(file) } + } + + val localVariableQuery = mutableMapOf>() + val localVariableHeaders = mutableMapOf() + + val localVariableConfig = RequestConfig( + RequestMethod.POST, + "/v1/bird/upload", + query = localVariableQuery, + headers = localVariableHeaders, + requiresAuthentication = false, + ) + + return multipartFormRequest( + localVariableConfig, + localVariableBody, + localVariableAuthNames + ).wrap() + } + + } diff --git a/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-multiplatform-allOf-discriminator/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/PetApi.kt index b13983df903e..72c008a77f55 100644 --- a/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/PetApi.kt @@ -25,6 +25,10 @@ import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine import kotlinx.serialization.json.Json import io.ktor.http.ParametersBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.ContentType +import io.ktor.http.content.PartData import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* diff --git a/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/StoreApi.kt index f388648f25ea..4d0a2f338e0a 100644 --- a/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -24,6 +24,10 @@ import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine import kotlinx.serialization.json.Json import io.ktor.http.ParametersBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.ContentType +import io.ktor.http.content.PartData import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* diff --git a/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/UserApi.kt index 0cfcbb91c758..5347a428fe97 100644 --- a/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/apis/UserApi.kt @@ -24,6 +24,10 @@ import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine import kotlinx.serialization.json.Json import io.ktor.http.ParametersBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.ContentType +import io.ktor.http.content.PartData import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* diff --git a/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-multiplatform-kotlinx-datetime/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/PetApi.kt b/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/PetApi.kt index b13983df903e..72c008a77f55 100644 --- a/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/PetApi.kt +++ b/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/PetApi.kt @@ -25,6 +25,10 @@ import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine import kotlinx.serialization.json.Json import io.ktor.http.ParametersBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.ContentType +import io.ktor.http.content.PartData import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* diff --git a/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/StoreApi.kt b/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/StoreApi.kt index f388648f25ea..4d0a2f338e0a 100644 --- a/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/StoreApi.kt +++ b/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/StoreApi.kt @@ -24,6 +24,10 @@ import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine import kotlinx.serialization.json.Json import io.ktor.http.ParametersBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.ContentType +import io.ktor.http.content.PartData import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* diff --git a/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/UserApi.kt b/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/UserApi.kt index 0cfcbb91c758..5347a428fe97 100644 --- a/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/UserApi.kt +++ b/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/apis/UserApi.kt @@ -24,6 +24,10 @@ import io.ktor.client.request.forms.formData import io.ktor.client.engine.HttpClientEngine import kotlinx.serialization.json.Json import io.ktor.http.ParametersBuilder +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.ContentType +import io.ktor.http.content.PartData import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* diff --git a/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-multiplatform/src/commonMain/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-name-parameter-mappings/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-name-parameter-mappings/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 14546d820360..608a208cac4a 100644 --- a/samples/client/petstore/kotlin-name-parameter-mappings/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-name-parameter-mappings/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-name-parameter-mappings/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-name-parameter-mappings/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-name-parameter-mappings/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-name-parameter-mappings/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index c88b2656e4ec..8ed3a3eb901a 100644 --- a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ internal open class ApiClient(val baseUrl: String, val client: Call.Factory = de return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ internal open class ApiClient(val baseUrl: String, val client: Call.Factory = de * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ internal open class ApiClient(val baseUrl: String, val client: Call.Factory = de * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ internal open class ApiClient(val baseUrl: String, val client: Call.Factory = de if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index 2da623bb7079..d67639eef27f 100644 --- a/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-nonpublic/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ internal data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index b650bfc5f8fc..24ba4d333f1b 100644 --- a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-nullable/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index b650bfc5f8fc..24ba4d333f1b 100644 --- a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-string/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 6bc994077211..560d92783571 100644 --- a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-threetenbp/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/apis/EnumApi.kt b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/apis/EnumApi.kt index 82a64bcca37c..2cd6c8d9af19 100644 --- a/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/apis/EnumApi.kt +++ b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/apis/EnumApi.kt @@ -37,6 +37,7 @@ import org.openapitools.client.infrastructure.RequestMethod import org.openapitools.client.infrastructure.ResponseType import org.openapitools.client.infrastructure.Success import org.openapitools.client.infrastructure.toMultiValue +import org.openapitools.client.infrastructure.Serializer open class EnumApi(basePath: kotlin.String = defaultBasePath, client: Call.Factory = ApiClient.defaultClient) : ApiClient(basePath, client) { companion object { diff --git a/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index 64f8281e060a..82abe8c6e784 100644 --- a/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -87,6 +87,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -99,15 +118,37 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + // Note: Without a custom serializer, kotlinx.serialization cannot serialize Any? + // The custom serializer should be provided at PartConfig creation to capture type info + parameterToString(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -116,15 +157,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -146,11 +189,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin-uppercase-enum/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null ) diff --git a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt index b650bfc5f8fc..24ba4d333f1b 100644 --- a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt +++ b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/ApiClient.kt @@ -86,6 +86,25 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie return contentType ?: "application/octet-stream" } + /** + * Builds headers for a multipart form-data part. + * OkHttp requires Content-Type to be passed via the RequestBody parameter, not in headers. + * This function filters out Content-Type and builds the appropriate Content-Disposition header. + * + * @param name The field name + * @param headers The headers from the PartConfig (may include Content-Type) + * @param filename Optional filename for file uploads + * @return Headers object ready for addPart() + */ + protected fun buildPartHeaders(name: String, headers: Map, filename: String? = null): Headers { + val disposition = if (filename != null) { + "form-data; name=\"$name\"; filename=\"$filename\"" + } else { + "form-data; name=\"$name\"" + } + return (headers.filterKeys { it != "Content-Type" } + ("Content-Disposition" to disposition)).toHeaders() + } + /** * Adds a File to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -98,15 +117,35 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @see requestBody */ protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, file: File) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"") val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull() addPart( - partHeaders.toHeaders(), + buildPartHeaders(name, headers, file.name), file.asRequestBody(fileMediaType) ) } + /** + * Serializes a multipart body part based on its content type. + * Uses JSON serialization for application/json content types, otherwise converts to string. + * + * @param obj The object to serialize + * @param contentType The Content-Type header value, if any + * @param serializer Optional custom serializer (used for kotlinx.serialization to preserve type info) + * @return The serialized string representation + */ + protected fun serializePartBody(obj: Any?, contentType: String?, serializer: ((Any?) -> String)?): String { + // Use custom serializer if provided (for kotlinx.serialization with captured type info) + if (serializer != null) { + return serializer(obj) + } + + return if (contentType?.contains("json") == true) { + Serializer.moshi.adapter(Any::class.java).toJson(obj) + } else { + parameterToString(obj) + } + } + /** * Adds any type to a MultipartBody.Builder * Defined a helper in the requestBody method to not duplicate code @@ -115,15 +154,17 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie * @param name The field name to add in the request * @param headers The headers that are in the PartConfig * @param obj The field name to add in the request + * @param serializer Optional custom serializer for this part * @return The method returns Unit but the new Part is added to the Builder that the extension function is applying on * @see requestBody */ - protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: T?) { - val partHeaders = headers.toMutableMap() + - ("Content-Disposition" to "form-data; name=\"$name\"") + protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map, obj: Any?, serializer: ((Any?) -> String)? = null) { + val partContentType = headers["Content-Type"] + val partMediaType = partContentType?.toMediaTypeOrNull() + val partBody = serializePartBody(obj, partContentType, serializer) addPart( - partHeaders.toHeaders(), - parameterToString(obj).toRequestBody(null) + buildPartHeaders(name, headers), + partBody.toRequestBody(partMediaType) ) } @@ -145,11 +186,11 @@ open class ApiClient(val baseUrl: String, val client: Call.Factory = defaultClie if (it is File) { addPartToMultiPart(name, part.headers, it) } else { - addPartToMultiPart(name, part.headers, it) + addPartToMultiPart(name, part.headers, it, part.serializer) } } } - else -> addPartToMultiPart(name, part.headers, part.body) + else -> addPartToMultiPart(name, part.headers, part.body, part.serializer) } } }.build() diff --git a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt index be00e38fbaee..c2a5c99ec7e6 100644 --- a/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt +++ b/samples/client/petstore/kotlin/src/main/kotlin/org/openapitools/client/infrastructure/PartConfig.kt @@ -4,8 +4,16 @@ package org.openapitools.client.infrastructure * Defines a config object for a given part of a multi-part request. * NOTE: Headers is a Map because rfc2616 defines * multi-valued headers as csv-only. + * + * @property headers The headers for this part + * @property body The body content for this part + * @property serializer Optional custom serializer for JSON content. When provided, this will be + * used instead of the default serialization for parts with application/json + * content-type. This allows capturing type information at the call site to + * avoid issues with type erasure in kotlinx.serialization. */ data class PartConfig( val headers: MutableMap = mutableMapOf(), - val body: T? = null + val body: T? = null, + val serializer: ((Any?) -> String)? = null )