Skip to content

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

Merged
wing328 merged 3 commits into
OpenAPITools:masterfrom
khayashi4337:fix/kotlin-deepobject-wire-keys
Nov 11, 2026
Merged

wing328 merged 3 commits into
OpenAPITools:masterfrom
khayashi4337:fix/kotlin-deepobject-wire-keys

Conversation

@khayashi4337

@khayashi4337 khayashi4337 commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

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

Test Libraries Asserts
testDeepObjectParamsKeepOwnWireKeyPrefix (new) jvm-ktor, jvm-okhttp4 each 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 run Result
master + this PR 34 0 failures, 0 skipped
this PR with the KotlinClientCodegen.java and api.mustache changes reverted (tests kept; only the new test run) 2 2 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

khayashi4337 and others added 3 commits November 11, 2026 12:20
…eName-based

Two deepObject bugs in the kotlin-client query serialization:

1. propagateParamBaseNameToVars wrote x-kotlin-param-base-name onto
   CodegenProperty instances that come from DefaultCodegen.fromProperty()'s
   cache, so two deepObject parameters sharing a model/property overwrote
   each other's wire-key prefix. With `filter-a` and `filter-b` sharing one
   schema, jvm-ktor emitted `localVariableQuery["filter-b[field]"]` for BOTH
   parameters. Vars are now cloned before the parameter-local extension is
   attached.

2. jvm-okhttp built the deepObject wire key from paramName
   (`put("filterA[field]")`), so hyphenated/renamed parameters sent a key
   that does not match the spec baseName. The template now uses the same
   x-kotlin-param-base-name extension as jvm-ktor, so the key is
   `filter-a[field]` regardless of the Kotlin identifier.

New fixture kotlin-deepobject-shared-vars.yaml + a jvm-ktor/jvm-okhttp4
test asserting each parameter keeps its own baseName on the wire.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The deepObject wire key now uses the spec baseName (query_object)
instead of the camelized paramName (queryObject).

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address review findings on the shared-vars fix:

- Rename the fixture property `field` to `status`: `field` is a Kotlin
  keyword, so the generated accessor is backticked and `{{name}}`
  interpolations emit the HTML-escaped `` `field` `` in jvm-okhttp,
  which cannot compile (a pre-existing template bug, out of scope here).
- Assert the generated key/value pairs, not just the keys: the
  jvm-okhttp4 `put("filter-a[status]", listOf(filterA.status.toString()))`
  and jvm-ktor `localVariableQuery["filter-a[status]"]` lines now verify
  that each wire key reads its own parameter's property.
- Render the `x-kotlin-param-base-name` prefix with triple braces (like
  `{{{paramName}}}`) in the three jvm-okhttp put() sites so parameter
  names containing `'` or `&` are not HTML-escaped in the wire key.
  The regenerated kotlin-jvm-okhttp-echo-api sample is byte-identical.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 5 files

View guided diff | Re-trigger cubic

@wing328 wing328 added this to the 7.28.0 milestone Nov 11, 2026
@wing328
wing328 merged commit 308d63f into OpenAPITools:master Nov 11, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants