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)) } } }