Skip to content

Commit 1fcc769

Browse files
AnarchyGhostk.markelov
andauthored
[kotlin][jvm-ktor] Fix query parameters typed as type:object (Map and model) (#23763)
The query parameter block in jvm-ktor/api.mustache used a single toMultiValue(...) call for every container type. Map<K,V> is not Iterable, so any query parameter with schema `type: object, additionalProperties: ...` failed to compile. Likewise, query parameters with `type: object, properties: ...` compiled but emitted toString() of the generated data class into the URL instead of OAS-defined serialization. Split the queryParams block into three branches: - isModel -> deepObject: per-field, key = paramBaseName[fieldBaseName] form+explode=true: per-field, key = fieldBaseName form+explode=false: single entry, CSV of "field,value" pairs (non-null fields only) under baseName - isMap -> deepObject: forEach with baseName[$key] form+explode=true: forEach with bare $key form+explode=false: joinToString(",") under baseName - default -> unchanged (scalar/array path via toMultiValue or listOf) KotlinClientCodegen now exposes the outer parameter's OAS baseName on each generated field via the `x-kotlin-param-base-name` vendor extension. The jvm-ktor template uses it to build correct deepObject URL keys for models, since Mustache provides no access to the outer scope from inside {{#vars}}. Added KotlinClientCodegenApiTest#testJvmKtorQueryParamWithTypeObject covering the 9 (schema x style/explode) combinations. No existing sample changed (verified via ./bin/generate-samples.sh on the three jvm-ktor configs): the fix only activates new branches for previously broken cases. Co-authored-by: k.markelov <k.markelov@energochain.ru>
1 parent af58d9e commit 1fcc769

4 files changed

Lines changed: 163 additions & 0 deletions

File tree

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinClientCodegen.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,24 @@ private static boolean isMultipartType(List<Map<String, String>> consumes) {
11611161
public void postProcessParameter(CodegenParameter parameter) {
11621162
super.postProcessParameter(parameter);
11631163
adjustEnumRefDefault(parameter);
1164+
propagateParamBaseNameToVars(parameter);
1165+
}
1166+
1167+
/**
1168+
* For query parameters with `type: object, properties: ...`, expose the
1169+
* parameter's OAS baseName on each generated field via the
1170+
* `x-kotlin-param-base-name` vendor extension. Templates that iterate
1171+
* `vars` (e.g. jvm-ktor) need the outer baseName to build URL keys like
1172+
* `paramBaseName[fieldBaseName]` per the OAS deepObject style, and
1173+
* Mustache provides no access to the outer scope from inside `{{#vars}}`.
1174+
*/
1175+
private void propagateParamBaseNameToVars(CodegenParameter param) {
1176+
if (!param.isQueryParam || !param.isModel || param.vars == null) {
1177+
return;
1178+
}
1179+
for (CodegenProperty v : param.vars) {
1180+
v.vendorExtensions.put("x-kotlin-param-base-name", param.baseName);
1181+
}
11641182
}
11651183

11661184
/**

modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,41 @@ import com.fasterxml.jackson.databind.ObjectMapper
132132

133133
val localVariableQuery = mutableMapOf<String, List<String>>()
134134
{{#queryParams}}
135+
{{#isModel}}
136+
{{#isDeepObject}}
137+
{{#vars}}
138+
{{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{vendorExtensions.x-kotlin-param-base-name}}[{{baseName}}]"] = listOf("$it") }
139+
{{/vars}}
140+
{{/isDeepObject}}
141+
{{^isDeepObject}}
142+
{{#isExplode}}
143+
{{#vars}}
144+
{{{paramName}}}?.{{{name}}}?.let { localVariableQuery["{{baseName}}"] = listOf("$it") }
145+
{{/vars}}
146+
{{/isExplode}}
147+
{{^isExplode}}
148+
{{{paramName}}}?.let { _model -> listOfNotNull({{#vars}}_model.{{{name}}}?.let { "{{baseName}},$it" }{{^-last}}, {{/-last}}{{/vars}}).takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{baseName}}"] = listOf(it.joinToString(",")) } }
149+
{{/isExplode}}
150+
{{/isDeepObject}}
151+
{{/isModel}}
152+
{{^isModel}}
153+
{{#isMap}}
154+
{{#isDeepObject}}
155+
{{{paramName}}}?.forEach { (key, value) -> localVariableQuery["{{baseName}}[$key]"] = listOf("$value") }
156+
{{/isDeepObject}}
157+
{{^isDeepObject}}
158+
{{#isExplode}}
159+
{{{paramName}}}?.forEach { (key, value) -> localVariableQuery[key] = listOf("$value") }
160+
{{/isExplode}}
161+
{{^isExplode}}
162+
{{{paramName}}}?.takeIf { it.isNotEmpty() }?.let { localVariableQuery["{{baseName}}"] = listOf(it.entries.joinToString(",") { (k, v) -> "$k,$v" }) }
163+
{{/isExplode}}
164+
{{/isDeepObject}}
165+
{{/isMap}}
166+
{{^isMap}}
135167
{{{paramName}}}?.apply { localVariableQuery["{{baseName}}"] = {{#isContainer}}toMultiValue(this, "{{collectionFormat}}"){{/isContainer}}{{^isContainer}}listOf("${{{paramName}}}"){{/isContainer}} }
168+
{{/isMap}}
169+
{{/isModel}}
136170
{{/queryParams}}
137171

138172
val localVariableHeaders = mutableMapOf<String, String>()

modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,28 @@ public void testGeneratedApisUseExplicitDateTypeArgumentsForQuerySerialization(C
190190
assertFileNotContains(queryApi.toPath(), "parseDateToQueryString(it)");
191191
}
192192

193+
@Test
194+
public void testJvmKtorQueryParamWithTypeObject() throws IOException {
195+
OpenAPI openAPI = readOpenAPI("3_0/kotlin/jvm-ktor-type-object-query.yaml");
196+
197+
KotlinClientCodegen codegen = createCodegen(ClientLibrary.JVM_KTOR);
198+
DefaultGenerator generator = new DefaultGenerator();
199+
enableOnlyApiGeneration(generator);
200+
201+
List<File> files = generator.opts(createClientOptInput(openAPI, codegen)).generate();
202+
File defaultApi = files.stream().filter(file -> file.getName().equals("DefaultApi.kt")).findAny().orElseThrow();
203+
204+
assertFileContains(defaultApi.toPath(), "mapFormExplode?.forEach { (key, value) -> localVariableQuery[key]");
205+
assertFileContains(defaultApi.toPath(), "mapFormNoexplode?.takeIf");
206+
assertFileContains(defaultApi.toPath(), "localVariableQuery[\"map_deep[$key]\"]");
207+
208+
assertFileContains(defaultApi.toPath(), "modelFormExplode?.a?.let { localVariableQuery[\"a\"]");
209+
assertFileContains(defaultApi.toPath(), "modelFormNoexplode?.let { _model -> listOfNotNull(_model.a?.let { \"a,$it\" }, _model.b?.let { \"b,$it\" })");
210+
assertFileContains(defaultApi.toPath(), "localVariableQuery[\"model_deep[a]\"]");
211+
212+
assertFileNotContains(defaultApi.toPath(), "mapDeep?.apply {");
213+
}
214+
193215
private static void assertFileContainsLine(List<String> lines, String line) {
194216
Assert.assertListContains(lines, s -> s.equals(line), line);
195217
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
openapi: 3.0.3
2+
info:
3+
title: jvm-ktor type:object query param coverage
4+
version: 1.0.0
5+
paths:
6+
/probe:
7+
get:
8+
operationId: probe
9+
parameters:
10+
- name: scalar_form
11+
in: query
12+
schema:
13+
type: string
14+
- name: array_form_explode
15+
in: query
16+
style: form
17+
explode: true
18+
schema:
19+
type: array
20+
items:
21+
type: string
22+
- name: array_form_noexplode
23+
in: query
24+
style: form
25+
explode: false
26+
schema:
27+
type: array
28+
items:
29+
type: string
30+
- name: map_form_explode
31+
in: query
32+
style: form
33+
explode: true
34+
schema:
35+
type: object
36+
additionalProperties:
37+
type: string
38+
- name: map_form_noexplode
39+
in: query
40+
style: form
41+
explode: false
42+
schema:
43+
type: object
44+
additionalProperties:
45+
type: string
46+
- name: map_deep
47+
in: query
48+
style: deepObject
49+
explode: true
50+
schema:
51+
type: object
52+
additionalProperties:
53+
type: string
54+
- name: model_form_explode
55+
in: query
56+
style: form
57+
explode: true
58+
schema:
59+
type: object
60+
properties:
61+
a:
62+
type: string
63+
b:
64+
type: integer
65+
- name: model_form_noexplode
66+
in: query
67+
style: form
68+
explode: false
69+
schema:
70+
type: object
71+
properties:
72+
a:
73+
type: string
74+
b:
75+
type: integer
76+
- name: model_deep
77+
in: query
78+
style: deepObject
79+
explode: true
80+
schema:
81+
type: object
82+
properties:
83+
a:
84+
type: string
85+
b:
86+
type: integer
87+
responses:
88+
'200':
89+
description: ok

0 commit comments

Comments
 (0)