diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java
index 19f5db5d6078..7afd28b81f90 100755
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java
@@ -1378,14 +1378,23 @@ public void postProcessParameter(CodegenParameter parameter) {
* `vars` (e.g. jvm-ktor) need the outer baseName to build URL keys like
* `paramBaseName[fieldBaseName]` per the OAS deepObject style, and
* Mustache provides no access to the outer scope from inside `{{#vars}}`.
+ *
+ *
The vars come from {@code DefaultCodegen.fromProperty()}'s cache, so
+ * the same instance can back multiple parameters; they are cloned before
+ * the parameter-local extension is attached to avoid leaking one
+ * parameter's baseName into another's wire keys.
*/
private void propagateParamBaseNameToVars(CodegenParameter param) {
if (!param.isQueryParam || !param.isModel || param.vars == null) {
return;
}
+ List vars = new ArrayList<>(param.vars.size());
for (CodegenProperty v : param.vars) {
- v.vendorExtensions.put("x-kotlin-param-base-name", param.baseName);
+ CodegenProperty copy = v.clone();
+ copy.vendorExtensions.put("x-kotlin-param-base-name", param.baseName);
+ vars.add(copy);
}
+ param.vars = vars;
}
/**
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache
index 8f412c86ee54..4bd02a0fde58 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache
@@ -215,7 +215,7 @@ import {{packageName}}.infrastructure.Serializer
{{#isModel}}
{{#vars}}
if ({{{paramName}}}.{{name}} != null) {
- put("{{#isDeepObject}}{{{paramName}}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
+ put("{{#isDeepObject}}{{#lambda.escapeDollar}}{{{vendorExtensions.x-kotlin-param-base-name}}}{{/lambda.escapeDollar}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
}
{{/vars}}
{{/isModel}}
@@ -230,7 +230,7 @@ import {{packageName}}.infrastructure.Serializer
{{#isModel}}
{{#vars}}
if ({{{paramName}}}.{{name}} != null) {
- put("{{#isDeepObject}}{{{paramName}}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
+ put("{{#isDeepObject}}{{#lambda.escapeDollar}}{{{vendorExtensions.x-kotlin-param-base-name}}}{{/lambda.escapeDollar}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
}
{{/vars}}
{{/isModel}}
@@ -243,7 +243,7 @@ import {{packageName}}.infrastructure.Serializer
{{#isModel}}
{{#vars}}
if ({{{paramName}}}.{{name}} != null) {
- put("{{#isDeepObject}}{{{paramName}}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
+ put("{{#isDeepObject}}{{#lambda.escapeDollar}}{{{vendorExtensions.x-kotlin-param-base-name}}}{{/lambda.escapeDollar}}[{{/isDeepObject}}{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}{{#isDeepObject}}]{{/isDeepObject}}", {{#isContainer}}toMultiValue({{{paramName}}}.{{name}}.toList(), "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf({{#isDateTime}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDateTime}}{{#isDate}}parseDateToQueryString<{{{dataType}}}>({{{paramName}}}.{{name}}){{/isDate}}{{#isEnum}}{{#isString}}{{{paramName}}}.{{name}}.value{{/isString}}{{^isString}}{{{paramName}}}.{{name}}.toString(){{/isString}}{{/isEnum}}{{^isEnum}}{{^isDateTime}}{{^isDate}}{{{paramName}}}.{{name}}.toString(){{/isDate}}{{/isDateTime}}{{/isEnum}}){{/isContainer}})
}
{{/vars}}
{{/isModel}}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java
index 51208d4130e8..eb9ae0500bcf 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java
@@ -318,6 +318,43 @@ public void testExplodedObjectQueryParameterJvmOkhttp() throws IOException {
"exploded entries must be added after the declared query parameters");
}
+ @DataProvider(name = "librariesWithDeepObjectWireKeys")
+ public static Object[][] librariesWithDeepObjectWireKeys() {
+ return new Object[][]{
+ {ClientLibrary.JVM_KTOR},
+ {ClientLibrary.JVM_OKHTTP4}
+ };
+ }
+
+ // Two deepObject query parameters share the property instances of the same
+ // $ref'd schema (fromProperty cache). The per-parameter wire-key prefix must
+ // stay parameter-local: filter-a[...] must not be overwritten by filter-b's
+ // propagation. jvm-okhttp prefixes keys with the spec baseName (via
+ // x-kotlin-param-base-name), not the camelized paramName.
+ @Test(dataProvider = "librariesWithDeepObjectWireKeys")
+ public void testDeepObjectParamsKeepOwnWireKeyPrefix(ClientLibrary library) throws IOException {
+ OpenAPI openAPI = readOpenAPI("src/test/resources/3_0/kotlin/kotlin-deepobject-shared-vars.yaml");
+
+ KotlinClientCodegen codegen = createCodegen(library);
+ DefaultGenerator generator = new DefaultGenerator();
+ enableOnlyApiGeneration(generator);
+
+ List files = generator.opts(createClientOptInput(openAPI, codegen)).generate();
+ File searchApi = files.stream().filter(file -> file.getName().equals("DefaultApi.kt")).findAny().orElseThrow();
+
+ // the wire key must be paired with the value of the parameter it names:
+ // filter-a[status] must read filterA's property, filter-b[status] filterB's
+ if (library == ClientLibrary.JVM_KTOR) {
+ assertFileContains(searchApi.toPath(),
+ "filterA?.status?.let { localVariableQuery[\"filter-a[status]\"] = listOf(\"$it\") }",
+ "filterB?.status?.let { localVariableQuery[\"filter-b[status]\"] = listOf(\"$it\") }");
+ } else {
+ assertFileContains(searchApi.toPath(),
+ "put(\"filter-a[status]\", listOf(filterA.status.toString()))",
+ "put(\"filter-b[status]\", listOf(filterB.status.toString()))");
+ }
+ }
+
private static void assertFileContainsLine(List lines, String line) {
Assert.assertListContains(lines, s -> s.equals(line), line);
}
diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-deepobject-shared-vars.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-deepobject-shared-vars.yaml
new file mode 100644
index 000000000000..a79d6bc53ea3
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/kotlin-deepobject-shared-vars.yaml
@@ -0,0 +1,31 @@
+openapi: 3.0.3
+info:
+ title: kotlin deepObject vars repro
+ version: 1.0.0
+paths:
+ /search:
+ get:
+ operationId: search
+ parameters:
+ - name: filter-a
+ in: query
+ style: deepObject
+ explode: true
+ schema:
+ $ref: '#/components/schemas/Filter'
+ - name: filter-b
+ in: query
+ style: deepObject
+ explode: true
+ schema:
+ $ref: '#/components/schemas/Filter'
+ responses:
+ '200':
+ description: ok
+components:
+ schemas:
+ Filter:
+ type: object
+ properties:
+ status:
+ type: string
diff --git a/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/apis/QueryApi.kt b/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/apis/QueryApi.kt
index 28caeef71df9..bbb2fc0a9b58 100644
--- a/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/apis/QueryApi.kt
+++ b/samples/client/echo_api/kotlin-jvm-okhttp/src/main/kotlin/org/openapitools/client/apis/QueryApi.kt
@@ -400,22 +400,22 @@ open class QueryApi(basePath: kotlin.String = defaultBasePath, client: Call.Fact
.apply {
if (queryObject != null) {
if (queryObject.id != null) {
- put("queryObject[id]", listOf(queryObject.id.toString()))
+ put("query_object[id]", listOf(queryObject.id.toString()))
}
if (queryObject.name != null) {
- put("queryObject[name]", listOf(queryObject.name.toString()))
+ put("query_object[name]", listOf(queryObject.name.toString()))
}
if (queryObject.category != null) {
- put("queryObject[category]", listOf(queryObject.category.toString()))
+ put("query_object[category]", listOf(queryObject.category.toString()))
}
if (queryObject.photoUrls != null) {
- put("queryObject[photoUrls]", toMultiValue(queryObject.photoUrls.toList(), ""))
+ put("query_object[photoUrls]", toMultiValue(queryObject.photoUrls.toList(), ""))
}
if (queryObject.tags != null) {
- put("queryObject[tags]", toMultiValue(queryObject.tags.toList(), ""))
+ put("query_object[tags]", toMultiValue(queryObject.tags.toList(), ""))
}
if (queryObject.status != null) {
- put("queryObject[status]", listOf(queryObject.status.value))
+ put("query_object[status]", listOf(queryObject.status.value))
}
}
}