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 fe2407cd128c..6d6230b2c94c 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 @@ -1161,6 +1161,24 @@ private static boolean isMultipartType(List> consumes) { public void postProcessParameter(CodegenParameter parameter) { super.postProcessParameter(parameter); adjustEnumRefDefault(parameter); + propagateParamBaseNameToVars(parameter); + } + + /** + * For query parameters with `type: object, properties: ...`, expose the + * parameter's OAS baseName on each generated field via the + * `x-kotlin-param-base-name` vendor extension. Templates that iterate + * `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}}`. + */ + private void propagateParamBaseNameToVars(CodegenParameter param) { + if (!param.isQueryParam || !param.isModel || param.vars == null) { + return; + } + for (CodegenProperty v : param.vars) { + v.vendorExtensions.put("x-kotlin-param-base-name", param.baseName); + } } /** diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache index 8b2dbf4a3cdd..bdffe7382d58 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache @@ -132,7 +132,41 @@ import com.fasterxml.jackson.databind.ObjectMapper val localVariableQuery = mutableMapOf>() {{#queryParams}} + {{#isModel}} + {{#isDeepObject}} + {{#vars}} + {{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{vendorExtensions.x-kotlin-param-base-name}}[{{baseName}}]"] = listOf("$it") } + {{/vars}} + {{/isDeepObject}} + {{^isDeepObject}} + {{#isExplode}} + {{#vars}} + {{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{baseName}}"] = listOf("$it") } + {{/vars}} + {{/isExplode}} + {{^isExplode}} + {{{paramName}}}?.let { _model -> listOfNotNull({{#vars}}_model.{{{name}}}?.let { "{{baseName}},$it" }{{^-last}}, {{/-last}}{{/vars}}).takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{baseName}}"] = listOf(it.joinToString(",")) } } + {{/isExplode}} + {{/isDeepObject}} + {{/isModel}} + {{^isModel}} + {{#isMap}} + {{#isDeepObject}} + {{{paramName}}}?.forEach { (key, value) -> localVariableQuery["{{baseName}}[$key]"] = listOf("$value") } + {{/isDeepObject}} + {{^isDeepObject}} + {{#isExplode}} + {{{paramName}}}?.forEach { (key, value) -> localVariableQuery[key] = listOf("$value") } + {{/isExplode}} + {{^isExplode}} + {{{paramName}}}?.takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{baseName}}"] = listOf(it.entries.joinToString(",") { (k, v) -> "$k,$v" }) } + {{/isExplode}} + {{/isDeepObject}} + {{/isMap}} + {{^isMap}} {{{paramName}}}?.apply { localVariableQuery["{{baseName}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf("${{{paramName}}}"){{/isContainer}} } + {{/isMap}} + {{/isModel}} {{/queryParams}} val localVariableHeaders = mutableMapOf() 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 dd0c30ccf6ac..b5466936da39 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 @@ -190,6 +190,28 @@ public void testGeneratedApisUseExplicitDateTypeArgumentsForQuerySerialization(C assertFileNotContains(queryApi.toPath(), "parseDateToQueryString(it)"); } + @Test + public void testJvmKtorQueryParamWithTypeObject() throws IOException { + OpenAPI openAPI = readOpenAPI("3_0/kotlin/jvm-ktor-type-object-query.yaml"); + + KotlinClientCodegen codegen = createCodegen(ClientLibrary.JVM_KTOR); + DefaultGenerator generator = new DefaultGenerator(); + enableOnlyApiGeneration(generator); + + List files = generator.opts(createClientOptInput(openAPI, codegen)).generate(); + File defaultApi = files.stream().filter(file -> file.getName().equals("DefaultApi.kt")).findAny().orElseThrow(); + + assertFileContains(defaultApi.toPath(), "mapFormExplode?.forEach { (key, value) -> localVariableQuery[key]"); + assertFileContains(defaultApi.toPath(), "mapFormNoexplode?.takeIf"); + assertFileContains(defaultApi.toPath(), "localVariableQuery[\"map_deep[$key]\"]"); + + assertFileContains(defaultApi.toPath(), "modelFormExplode?.a?.let { localVariableQuery[\"a\"]"); + assertFileContains(defaultApi.toPath(), "modelFormNoexplode?.let { _model -> listOfNotNull(_model.a?.let { \"a,$it\" }, _model.b?.let { \"b,$it\" })"); + assertFileContains(defaultApi.toPath(), "localVariableQuery[\"model_deep[a]\"]"); + + assertFileNotContains(defaultApi.toPath(), "mapDeep?.apply {"); + } + 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/jvm-ktor-type-object-query.yaml b/modules/openapi-generator/src/test/resources/3_0/kotlin/jvm-ktor-type-object-query.yaml new file mode 100644 index 000000000000..ee81cd825ce2 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/jvm-ktor-type-object-query.yaml @@ -0,0 +1,89 @@ +openapi: 3.0.3 +info: + title: jvm-ktor type:object query param coverage + version: 1.0.0 +paths: + /probe: + get: + operationId: probe + parameters: + - name: scalar_form + in: query + schema: + type: string + - name: array_form_explode + in: query + style: form + explode: true + schema: + type: array + items: + type: string + - name: array_form_noexplode + in: query + style: form + explode: false + schema: + type: array + items: + type: string + - name: map_form_explode + in: query + style: form + explode: true + schema: + type: object + additionalProperties: + type: string + - name: map_form_noexplode + in: query + style: form + explode: false + schema: + type: object + additionalProperties: + type: string + - name: map_deep + in: query + style: deepObject + explode: true + schema: + type: object + additionalProperties: + type: string + - name: model_form_explode + in: query + style: form + explode: true + schema: + type: object + properties: + a: + type: string + b: + type: integer + - name: model_form_noexplode + in: query + style: form + explode: false + schema: + type: object + properties: + a: + type: string + b: + type: integer + - name: model_deep + in: query + style: deepObject + explode: true + schema: + type: object + properties: + a: + type: string + b: + type: integer + responses: + '200': + description: ok