fix(kotlin-client): avoid name shadowing in ktor/multiplatform form bodies and responses
Fixes name-resolution bugs in code generated by the kotlin client: in the urlencoded and multipart form bodies of library=jvm-ktor and library=multiplatform, and in how library=multiplatform unwraps array and map responses. Depending on the spec, the generated client fails to compile, or sends or returns the wrong value.
Bugs and reproducers
All three specs are added by this PR under modules/openapi-generator/src/test/resources/3_0/kotlin/.
1. A form field named it (kotlin-form-it-param.yaml). The urlencoded body is built inside ParametersBuilder().also { … }, whose implicit lambda parameter it is the builder. The form field it becomes a function parameter, generated as `it`. Inside the lambda, the implicit it (the builder) shadows that function parameter, so the field's value is never read:
- jvm-ktor emits
`it`?.apply { it.append("it", `it`) }. The value argument is the builder, a type mismatch, so the client does not compile. - multiplatform emits
`it`?.apply { it.append("it", `it`.toString()) }. This compiles, but sends the builder's string representation instead of the caller's value.
2. Urlencoded array fields call a receiver-less append (jvm-ktor). Inside .also { } the builder is the lambda parameter it, not the receiver, so append("tags", x.toString()) in the array loop does not call the builder.
3. A multipart file field named my-file (kotlin-multipart-file.yaml, multiplatform). multiplatform emits myFile?.apply { append(my-file) }. The wire name is emitted as an expression, which Kotlin reads as my - file, two undefined identifiers, so the client does not compile.
4. A parameter named value (kotlin-receiver-value.yaml, multiplatform). Operations that return an array or a map end with wrap<…Response>().map { value }. map takes a receiver lambda (T.() -> V), and value is meant to be the response wrapper's field. When the operation also has a parameter named value, Kotlin resolves value to the parameter first, so the function returns the caller's argument instead of the decoded response, or fails to compile when the types differ.
Fix
- The urlencoded builder is bound to an explicit name,
ParametersBuilder().also { localVariableBuilder -> … }, and everyappendinside it, including the array loop, callslocalVariableBuilder.append(…)(jvm-ktor, multiplatform). - multiplatform's multipart file part appends the parameter,
append({{{paramName}}}). - multiplatform's wrapper maps read
this.value(.map { this.value }and.map { this.value.toTypedArray() }).
The new name follows the localVariable… prefix the templates already use for generated locals (localVariableConfig, localVariableBody, …). As with those, a form field literally named localVariableBuilder would still collide. That is the same existing class of collision and is not addressed here.
Not changed here: multiplatform sends a urlencoded array field as tags.toString() ([a, b]). That is a separate issue; the test below checks the tags line only for jvm-ktor.
Tests
| Test (new) | Libraries | Asserts |
|---|---|---|
testKtorMultiplatformFormBuilderDoesNotUseImplicitIt | jvm-ktor, multiplatform | localVariableBuilder.append("it" is emitted, and no it.append("it" / it.append("name"; for jvm-ktor also localVariableBuilder.append("tags" |
testKtorMultiplatformMultipartFileAppendUsesParameter | jvm-ktor, multiplatform | append(myFile) is emitted and append(my-file) is not (jvm-ktor already emits this on master; the row guards it) |
testMultiplatformWrapMapReceiverIsThisValue | multiplatform | map { this.value } is emitted and map { value } is not |
./mvnw -nsu -Denforcer.skip=true -pl modules/openapi-generator -am -Dtest=KotlinClientCodegenApiTest -Dsurefire.failIfNoSpecifiedTests=false test
| Tests run | Result | |
|---|---|---|
| master + this PR | 37 | 0 failures, 0 skipped |
this PR with the two api.mustache changes reverted (tests kept; only the new tests run) | 5 | 4 failures — both rows of testKtorMultiplatformFormBuilderDoesNotUseImplicitIt, the multiplatform row of testKtorMultiplatformMultipartFileAppendUsesParameter, and testMultiplatformWrapMapReceiverIsThisValue |
Samples
8 files changed in kotlin-jvm-ktor-gson, kotlin-jvm-ktor-jackson, kotlin-jvm-ktor-kotlinx_serialization, kotlin-multiplatform and kotlin-multiplatform-kotlinx-datetime. The petstore specs have none of the names above, so the only changes are the builder's name and this.value. All five directories are compiled by samples-kotlin-client.yaml.
This PR and its sibling PRs #25622, #25623 and #25625 ($ escaping in wire names) all touch KotlinClientCodegenApiTest and neighboring kotlin-client templates; whichever merges first, I will rebase the others.
cc @wing328 @karismann @Zomzog @andrewemery @4brunu @yutaka0m @stefankoppier @e5l (Kotlin technical committee)
PR checklist
- Read the contribution guidelines.
- Run the following to build the project and update samples — scoped to this change: built the CLI (
./mvnw -pl modules/openapi-generator-cli -am package -DskipTests) and regenerated the jvm-ktor and multiplatform configs with./bin/generate-samples.sh; the resulting diff is committed.export_docs_generators.shnot run: no generator option or metadata changes.
(For Windows users, please run the script in WSL)./mvnw clean package || exit ./bin/generate-samples.sh ./bin/configs/*.yaml || exit ./bin/utils/export_docs_generators.sh || exit
Commit all changed files.
This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
These must match the expectations made by your contribution.
You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example./bin/generate-samples.sh bin/configs/java*.
IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed. - If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.
🤖 Generated with Claude Code