fix(kotlin-client): keep deepObject wire keys parameter-local and baseName-based

Merged
KH
2d ago
+87-10

Fixes two bugs in how the kotlin client builds query keys for style: deepObject parameters. In both, the key put on the wire does not match the parameter name declared in the spec.

  1. jvm-ktor: one parameter's key prefix leaks into another's. KotlinClientCodegen.propagateParamBaseNameToVars writes x-kotlin-param-base-name onto each CodegenProperty in param.vars. Those property instances come from DefaultCodegen.fromProperty()'s cache, so two deepObject parameters that use the same schema share them, and the parameter processed last overwrites the prefix of the other.
  2. jvm-okhttp: the key prefix is the Kotlin identifier, not the spec name. jvm-okhttp/api.mustache (the template behind library=jvm-okhttp4) builds the key from {{{paramName}}} (camelized), so a parameter declared as query_object is sent as queryObject[id].

Reproducer

modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-deepobject-shared-vars.yaml (added by this PR): two deepObject query parameters, filter-a and filter-b, both $ref: '#/components/schemas/Filter' (one string property status).

On master:

  • jvm-ktor emits localVariableQuery["filter-b[status]"] for both parameters, so the filter-a value is sent under filter-b[status].
  • jvm-okhttp4 emits put("filterA[status]", …) and put("filterB[status]", …). Neither key matches the declared names filter-a / filter-b.

With this PR, both libraries emit filter-a[status] for filter-a and filter-b[status] for filter-b.

The existing echo_api sample shows bug 2 on a real spec. samples/client/echo_api/kotlin-jvm-okhttp/.../QueryApi.kt sends queryObject[id], queryObject[name], … for the parameter declared as query_object. After regeneration it sends query_object[id], query_object[name], ….

Fix

  • propagateParamBaseNameToVars clones each var (CodegenProperty.clone()) before attaching the parameter-local extension, and assigns the copies to param.vars. The cached instances are no longer modified.
  • jvm-okhttp/api.mustache: the three put("…[…]") sites use {{{vendorExtensions.x-kotlin-param-base-name}}} (through lambda.escapeDollar, as the property part already does), the same source jvm-ktor uses. Triple braces keep the name unescaped, as {{{paramName}}} did before.

Behavior change

For jvm-okhttp clients, the query key of a deepObject parameter whose spec name differs from its Kotlin identifier changes from the identifier to the spec name, e.g. queryObject[id] → query_object[id]. The new key is what the spec declares. A server that only accepted the old key would see different keys after regenerating the client.

Tests

TestLibrariesAsserts
testDeepObjectParamsKeepOwnWireKeyPrefix (new)jvm-ktor, jvm-okhttp4each key is paired with its own parameter, e.g. filterA?.status?.let { localVariableQuery["filter-a[status]"] = listOf("$it") } (jvm-ktor) and put("filter-a[status]", listOf(filterA.status.toString())) (jvm-okhttp4), and the same for filter-b

./mvnw -nsu -Denforcer.skip=true -pl modules/openapi-generator -am -Dtest=KotlinClientCodegenApiTest -Dsurefire.failIfNoSpecifiedTests=false test

Tests runResult
master + this PR340 failures, 0 skipped
this PR with the KotlinClientCodegen.java and api.mustache changes reverted (tests kept; only the new test run)22 failures — both rows of testDeepObjectParamsKeepOwnWireKeyPrefix

Samples

Only samples/client/echo_api/kotlin-jvm-okhttp changed (1 file, QueryApi.kt, the keys shown above). It is built by samples-kotlin-echo-api.yaml.

This PR and its sibling PRs #25623, #25624 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 affected kotlin configs with ./bin/generate-samples.sh; the resulting diff is committed. export_docs_generators.sh not run: no generator option or metadata changes.
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    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