Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/samples-kotlin-echo-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ 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-okhttp/build/
samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/build/

# haskell
.stack-work
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ import com.squareup.moshi.adapter
* @see requestBody
*/
protected fun MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, file: File) {
val partHeaders = headers.toMutableMap() +
// Filter out Content-Type from headers as OkHttp requires it to be passed
// separately via asRequestBody(mediaType), not in the headers map
val partHeaders = headers.filterKeys { it != "Content-Type" }.toMutableMap() +
("Content-Disposition" to "form-data; name=\"$name\"; filename=\"${file.name}\"")
val fileMediaType = guessContentTypeFromFile(file).toMediaTypeOrNull()
addPart(
Expand All @@ -148,11 +150,31 @@ import com.squareup.moshi.adapter
* @see requestBody
*/
protected fun <T> MultipartBody.Builder.addPartToMultiPart(name: String, headers: Map<String, String>, obj: T?) {
val partHeaders = headers.toMutableMap() +
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) {
{{#moshi}}
Serializer.moshi.adapter(Any::class.java).toJson(obj)
{{/moshi}}
{{#gson}}
Serializer.gson.toJson(obj)
{{/gson}}
{{#jackson}}
Serializer.jacksonObjectMapper.writeValueAsString(obj)
{{/jackson}}
{{#kotlinx_serialization}}
Serializer.kotlinxSerializationJson.encodeToString(obj)
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Multipart JSON parts using Kotlinx Serialization will throw at runtime because encodeToString(obj) is invoked with type‑erased Any (no serializer), which requires polymorphic registration.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/infrastructure/ApiClient.kt.mustache, line 170:

<comment>Multipart JSON parts using Kotlinx Serialization will throw at runtime because `encodeToString(obj)` is invoked with type‑erased `Any` (no serializer), which requires polymorphic registration.</comment>

<file context>
@@ -148,11 +150,31 @@ import com.squareup.moshi.adapter
+            Serializer.jacksonObjectMapper.writeValueAsString(obj)
+            {{/jackson}}
+            {{#kotlinx_serialization}}
+            Serializer.kotlinxSerializationJson.encodeToString(obj)
+            {{/kotlinx_serialization}}
+        } else {
</file context>
Fix with Cubic

{{/kotlinx_serialization}}
} else {
parameterToString(obj)
}
addPart(
partHeaders.toHeaders(),
parameterToString(obj).toRequestBody(null)
partBody.toRequestBody(partMediaType)
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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"]
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7.20.0-SNAPSHOT
61 changes: 61 additions & 0 deletions samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/README.md
Original file line number Diff line number Diff line change
@@ -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.

<a id="documentation-for-api-endpoints"></a>
## 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 |


<a id="documentation-for-models"></a>
## Documentation for Models

- [org.openapitools.client.models.FileMetadata](docs/FileMetadata.md)


<a id="documentation-for-authorization"></a>
## Documentation for Authorization

Endpoints do not require authorization.

Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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 |


<a id="testBodyMultipartFormdataWithJsonPart"></a>
# **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 |
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The Kotlin example declares metadata with an = but provides no value, producing invalid code that users cannot compile or run.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/BodyApi.md, line 25:

<comment>The Kotlin example declares `metadata` with an `=` but provides no value, producing invalid code that users cannot compile or run.</comment>

<file context>
@@ -0,0 +1,57 @@
+//import org.openapitools.client.models.*
+
+val apiInstance = BodyApi()
+val metadata : FileMetadata =  // FileMetadata | 
+val file : java.io.File = BINARY_DATA_HERE // java.io.File | File to upload
+try {
</file context>
Fix with Cubic

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)| | |
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Malformed Markdown table: header/separator rows come after the first data row, so metadata is rendered outside the table.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/echo_api/kotlin-jvm-okhttp-multipart-json/docs/BodyApi.md, line 40:

<comment>Malformed Markdown table: header/separator rows come after the first data row, so `metadata` is rendered outside the table.</comment>

<file context>
@@ -0,0 +1,57 @@
+```
+
+### Parameters
+| **metadata** | [**FileMetadata**](FileMetadata.md)|  | |
+| Name | Type | Description  | Notes |
+| ------------- | ------------- | ------------- | ------------- |
</file context>
Fix with Cubic

| 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

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

# FileMetadata

## Properties
| Name | Type | Description | Notes |
| ------------ | ------------- | ------------- | ------------- |
| **id** | **kotlin.Long** | | |
| **name** | **kotlin.String** | | |
| **tags** | **kotlin.collections.List&lt;kotlin.String&gt;** | | [optional] |



Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading