[kotlin] fix: explode object query parameters

Merged
WI
1mo ago
+117-0

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
expectedcategory=books&createdDate%3Agte=2023-01-01
kotlin before, free-form filterfilter=%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:

parameteron 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

  • Read the contribution guidelines.
  • Built the project and updated samples (./bin/generate-samples.sh bin/configs/kotlin*.yaml): no sample files change, since no kotlin sample spec has a free-form or map-typed query parameter with the default style.
  • Technical committee: @jimschubert @andrewemery @alisters @stefankoppier

Generated with Claude Code