Skip to content

[kotlin] fix: explode object query parameters - #24867

Open
wiebren wants to merge 4 commits into
OpenAPITools:masterfrom
wiebren:fix/exploded-object-query-parameters-kotlin
Open

wiebren wants to merge 4 commits into
OpenAPITools:masterfrom
wiebren:fix/exploded-object-query-parameters-kotlin

Conversation

@wiebren

@wiebren wiebren commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Bug

A query parameter whose schema is an object, with style/explode left at their defaults (form, true), must go on the wire as one parameter per entry, keyed by the property name alone. The kotlin client's jvm-okhttp library sent the whole map as a single toString() parameter instead; kotlin's own jvm-ktor library gets it right.

parameters:
  - in: query
    name: filter
    schema:
      type: object

called with {"category": "books", "createdDate:gte": "2023-01-01"}:

on the wire
expected category=books&createdDate%3Agte=2023-01-01
kotlin before, free-form filter filter=%7Bcategory%3Dbooks%2C%20createdDate%3Agte%3D2023-01-01%7D (Map.toString())
kotlin before, typed map (additionalProperties) typedFilter=%28category%2C%20books%29%28createdDate%3Agte%2C%202023-01-01%29 (Pair.toString()s run together)

Series

One of six per-language PRs for the same bug: #24797 go, #24802 python, #24803 typescript-fetch, #24867 kotlin, #24868 dart, #24869 ruby. No shared main/ code; each adds the same fixture, 3_0/exploded-object-query-param.yaml.

Fix

KotlinClientCodegen marks a query parameter that is isMap and isExplode and not isDeepObject with x-kotlin-explode-form-object (jvm-okhttp libraries only). jvm-okhttp/api.mustache leaves those out of its per-parameter loop and adds them in a second pass, after every declared query parameter. A free-form object is typed kotlin.Any?, hence a safe cast to Map<*, *>. Per entry:

  • a null value (or key) is left out rather than sent as null;
  • a collection or array repeats the key once per non-null element (tld=com&tld=net);
  • dates and other scalars go through parameterToString, like declared query parameters;
  • a name collision with another query parameter keeps both values, whatever the order, because the exploded entries are added last and append.

Every other parameter produces the line it did before.

Verified

A client generated from the fixture, against a server that echoes its raw query string:

parameter on the wire
filter (object, defaults) category=books&createdDate%3Agte=2023-01-01
typedFilter (map, defaults) category=books&createdDate%3Agte=2023-01-01
deepFilter (style: deepObject) deepFilter=%7Bcategory%3Dbooks%2C…%7D (as on master, see Known gaps)
flatFilter (explode: false) flatFilter=%7Bcategory%3Dbooks%2C…%7D (as on master, see Known gaps)

KotlinClientCodegenApiTest#testExplodedObjectQueryParameterJvmOkhttp fails without the fix. A runtime check on the generated client covers null entries, list, array and empty-list values, a LocalDate value, and a colliding name in both orders.

Known gaps

  • deepObject and explode: false still go out as one toString() parameter, as on master. Both are wrong per the spec (deepFilter[category]=books, flatFilter=category,books,…); left for a follow-up.
  • A non-map value in a free-form object parameter used to go out as filter=<toString()> and is now dropped by the safe cast. A hard cast would throw ClassCastException instead; happy to switch if preferred.
  • Other kotlin libraries are untouched: jvm-ktor already explodes correctly, and retrofit2, volley, vertx, spring-* and multiplatform have their own serialization paths.

PR checklist


Generated with Claude Code

A query parameter whose schema is an object and whose style/explode are left at
their defaults — style: form, explode: true — must go on the wire as one
parameter per entry, keyed by the property name alone. The jvm-okhttp client
serialized the whole map with toString() into a single parameter instead
(filter={tld=com, ...}), and a declared map came out as concatenated
Pair.toString()s. The jvm-ktor library already explodes these correctly.

The query building in api.mustache now iterates an exploded map entry by
entry, the same way jvm-ktor does. deepObject and explode: false objects
keep their previous wire format, byte for byte.

The new test fixture covers the four style/explode combinations that decide
the wire format; the test fails without the template change.

No sample changes: no kotlin sample spec declares a free-form or map-typed
query parameter with the default style, the echo api's object query
parameters are all model-typed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CxDNCjqJycKTfzVWg2SeTJ

@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.

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

…y entries

Follow-up to the review of the exploded object query parameters in the
jvm-okhttp client:

- a null map entry is left out instead of going on the wire as the
  literal "null" (Any?.toString() returns "null" rather than throwing)
- a collection value repeats the key once per element instead of being
  sent as the collection's toString(); scalar values and elements go
  through parameterToString, so dates get the same format as other query
  parameters
- an exploded entry named like another query parameter no longer
  overwrites it, or gets overwritten by it: the exploded parameters are
  now emitted after every declared query parameter and append to an
  existing entry, so both values end up on the wire

To place the exploded parameters after the declared ones, the codegen
flags them with x-kotlin-explode-form-object (jvm-okhttp libraries only)
and api.mustache emits them in a second pass; the three duplicated
required/optional/nullable branches of the first version are gone.
deepObject and explode: false parameters are unchanged, and no sample
changes.

Co-Authored-By: Claude Opus 5 (1M context) <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 4 files

Re-trigger cubic

wiebren and others added 2 commits September 23, 2026 08:59
…y loop

One comment line above the loop instead of three; the when now only picks
the elements and filterNotNull/parameterToString run once. Drop the
defensive !isModel check, which no other port has: a map-typed parameter
is never isModel. Fix the fixture description, which claimed four
style/explode combinations where it covers three, and retitle the test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ports

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant