Skip to content

Commit 76e3258

Browse files
[Java][jersey2/jersey3] Fix serialization of array path parameters (#24986)
* [Java][jersey2/jersey3] Fix serialization of array path parameters Array path parameters were serialized with Collection.toString(), producing "[a, b, c]" in the URL. Add ApiClient.collectionPathParameterToString, which escapes each item individually and joins them with the delimiter for the parameter's collection format (csv by default). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * [Java][jersey2/jersey3] Address review feedback on array path params - Keep the delimiter before leading empty or null items in collectionPathParameterToString (e.g. ["", "b"] -> ",b") - Remove the unused pipedIds query param from the test spec - Add runtime tests for collectionPathParameterToString to the jersey2-java8 and jersey3 sample ApiClientTest Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent c3680af commit 76e3258

20 files changed

Lines changed: 575 additions & 2 deletions

File tree

‎modules/openapi-generator/src/main/resources/Java/libraries/jersey2/ApiClient.mustache‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,41 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
977977
}
978978
}
979979

980+
/**
981+
* Format the given collection as a path parameter value according to the collection format.
982+
* Each item is escaped individually, so that the delimiter is preserved (e.g. "a,b,c" for csv).
983+
*
984+
* @param collectionFormat Collection format (csv by default)
985+
* @param value Collection value
986+
* @return Escaped path parameter value
987+
*/
988+
public String collectionPathParameterToString(String collectionFormat, Collection<?> value) {
989+
if (value == null || value.isEmpty()) {
990+
return "";
991+
}
992+
993+
// "multi" is not valid for path params, fall back to csv
994+
String delimiter = ",";
995+
if ("ssv".equals(collectionFormat)) {
996+
delimiter = escapeString(" ");
997+
} else if ("tsv".equals(collectionFormat)) {
998+
delimiter = escapeString("\t");
999+
} else if ("pipes".equals(collectionFormat)) {
1000+
delimiter = escapeString("|");
1001+
}
1002+
1003+
StringBuilder sb = new StringBuilder();
1004+
boolean first = true;
1005+
for (Object item : value) {
1006+
if (!first) {
1007+
sb.append(delimiter);
1008+
}
1009+
sb.append(escapeString(parameterToString(item)));
1010+
first = false;
1011+
}
1012+
return sb.toString();
1013+
}
1014+
9801015
/**
9811016
* Serialize the given Java object into string entity according the given
9821017
* Content-Type (only JSON is supported for now).

‎modules/openapi-generator/src/main/resources/Java/libraries/jersey2/api.mustache‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public class {{classname}} {
137137
{{#hasPathParams}}
138138
// Path parameters
139139
String localVarPath = "{{{path}}}"{{#pathParams}}
140-
.replaceAll({{=% %=}}"\\{%baseName%}"%={{ }}=%, apiClient.escapeString({{{paramName}}}.toString())){{/pathParams}};
140+
.replaceAll({{=% %=}}"\\{%baseName%}"%={{ }}=%, {{#isArray}}apiClient.collectionPathParameterToString("{{{collectionFormat}}}", {{{paramName}}}){{/isArray}}{{^isArray}}apiClient.escapeString({{{paramName}}}.toString()){{/isArray}}){{/pathParams}};
141141

142142
{{/hasPathParams}}
143143
{{#queryParams}}

‎modules/openapi-generator/src/main/resources/Java/libraries/jersey3/ApiClient.mustache‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,41 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} {
983983
}
984984
}
985985

986+
/**
987+
* Format the given collection as a path parameter value according to the collection format.
988+
* Each item is escaped individually, so that the delimiter is preserved (e.g. "a,b,c" for csv).
989+
*
990+
* @param collectionFormat Collection format (csv by default)
991+
* @param value Collection value
992+
* @return Escaped path parameter value
993+
*/
994+
public String collectionPathParameterToString(String collectionFormat, Collection<?> value) {
995+
if (value == null || value.isEmpty()) {
996+
return "";
997+
}
998+
999+
// "multi" is not valid for path params, fall back to csv
1000+
String delimiter = ",";
1001+
if ("ssv".equals(collectionFormat)) {
1002+
delimiter = escapeString(" ");
1003+
} else if ("tsv".equals(collectionFormat)) {
1004+
delimiter = escapeString("\t");
1005+
} else if ("pipes".equals(collectionFormat)) {
1006+
delimiter = escapeString("|");
1007+
}
1008+
1009+
StringBuilder sb = new StringBuilder();
1010+
boolean first = true;
1011+
for (Object item : value) {
1012+
if (!first) {
1013+
sb.append(delimiter);
1014+
}
1015+
sb.append(escapeString(parameterToString(item)));
1016+
first = false;
1017+
}
1018+
return sb.toString();
1019+
}
1020+
9861021
/**
9871022
* Serialize the given Java object into string entity according the given
9881023
* Content-Type (only JSON is supported for now).

‎modules/openapi-generator/src/main/resources/Java/libraries/jersey3/api.mustache‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public class {{classname}} {
137137
{{#hasPathParams}}
138138
// Path parameters
139139
String localVarPath = "{{{path}}}"{{#pathParams}}
140-
.replaceAll({{=% %=}}"\\{%baseName%}"%={{ }}=%, apiClient.escapeString({{{paramName}}}.toString())){{/pathParams}};
140+
.replaceAll({{=% %=}}"\\{%baseName%}"%={{ }}=%, {{#isArray}}apiClient.collectionPathParameterToString("{{{collectionFormat}}}", {{{paramName}}}){{/isArray}}{{^isArray}}apiClient.escapeString({{{paramName}}}.toString()){{/isArray}}){{/pathParams}};
141141

142142
{{/hasPathParams}}
143143
{{#queryParams}}

‎modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5351,6 +5351,27 @@ public void testInsecureTlsHookOmittedWhenDisabled(String library) {
53515351
.doesNotContain("import java.security.cert.X509Certificate;");
53525352
}
53535353

5354+
@Test(dataProvider = "jerseyLibraries")
5355+
public void testArrayPathParamSerializedAsCollection(String library) {
5356+
Path output = newTempFolder();
5357+
CodegenConfigurator configurator = new CodegenConfigurator()
5358+
.setGeneratorName(JAVA_GENERATOR)
5359+
.setLibrary(library)
5360+
.addAdditionalProperty(CodegenConstants.INVOKER_PACKAGE, "xyz.abcdef.invoker")
5361+
.addAdditionalProperty(CodegenConstants.API_PACKAGE, "xyz.abcdef.api")
5362+
.setInputSpec("src/test/resources/3_0/java/array-path-param.yaml")
5363+
.setOutputDir(output.toString().replace("\\", "/"));
5364+
5365+
new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
5366+
5367+
assertThat(output.resolve("src/main/java/xyz/abcdef/api/DefaultApi.java")).content()
5368+
.contains("apiClient.collectionPathParameterToString(\"csv\", ids)")
5369+
.contains("apiClient.escapeString(id.toString())")
5370+
.doesNotContain("apiClient.escapeString(ids.toString())");
5371+
JavaFileAssert.assertThat(output.resolve("src/main/java/xyz/abcdef/invoker/ApiClient.java").toFile())
5372+
.assertMethod("collectionPathParameterToString", "String", "Collection<?>");
5373+
}
5374+
53545375
private static Path generateJerseyClient(String library, Boolean generateInsecureTlsHook) {
53555376
Path output = newTempFolder();
53565377
CodegenConfigurator configurator = new CodegenConfigurator()
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Array path param
4+
version: 1.0.0
5+
paths:
6+
/items/{ids}/action:
7+
get:
8+
operationId: getItems
9+
parameters:
10+
- name: ids
11+
in: path
12+
required: true
13+
schema:
14+
type: array
15+
items:
16+
type: string
17+
responses:
18+
'204':
19+
description: ok
20+
/items/{id}:
21+
get:
22+
operationId: getItem
23+
parameters:
24+
- name: id
25+
in: path
26+
required: true
27+
schema:
28+
type: string
29+
responses:
30+
'204':
31+
description: ok

‎samples/client/others/java/jersey2-oneOf-Mixed/src/main/java/org/openapitools/client/ApiClient.java‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,41 @@ public String escapeString(String str) {
753753
}
754754
}
755755

756+
/**
757+
* Format the given collection as a path parameter value according to the collection format.
758+
* Each item is escaped individually, so that the delimiter is preserved (e.g. "a,b,c" for csv).
759+
*
760+
* @param collectionFormat Collection format (csv by default)
761+
* @param value Collection value
762+
* @return Escaped path parameter value
763+
*/
764+
public String collectionPathParameterToString(String collectionFormat, Collection<?> value) {
765+
if (value == null || value.isEmpty()) {
766+
return "";
767+
}
768+
769+
// "multi" is not valid for path params, fall back to csv
770+
String delimiter = ",";
771+
if ("ssv".equals(collectionFormat)) {
772+
delimiter = escapeString(" ");
773+
} else if ("tsv".equals(collectionFormat)) {
774+
delimiter = escapeString("\t");
775+
} else if ("pipes".equals(collectionFormat)) {
776+
delimiter = escapeString("|");
777+
}
778+
779+
StringBuilder sb = new StringBuilder();
780+
boolean first = true;
781+
for (Object item : value) {
782+
if (!first) {
783+
sb.append(delimiter);
784+
}
785+
sb.append(escapeString(parameterToString(item)));
786+
first = false;
787+
}
788+
return sb.toString();
789+
}
790+
756791
/**
757792
* Serialize the given Java object into string entity according the given
758793
* Content-Type (only JSON is supported for now).

‎samples/client/others/java/jersey2-oneOf-duplicates/src/main/java/org/openapitools/client/ApiClient.java‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,41 @@ public String escapeString(String str) {
753753
}
754754
}
755755

756+
/**
757+
* Format the given collection as a path parameter value according to the collection format.
758+
* Each item is escaped individually, so that the delimiter is preserved (e.g. "a,b,c" for csv).
759+
*
760+
* @param collectionFormat Collection format (csv by default)
761+
* @param value Collection value
762+
* @return Escaped path parameter value
763+
*/
764+
public String collectionPathParameterToString(String collectionFormat, Collection<?> value) {
765+
if (value == null || value.isEmpty()) {
766+
return "";
767+
}
768+
769+
// "multi" is not valid for path params, fall back to csv
770+
String delimiter = ",";
771+
if ("ssv".equals(collectionFormat)) {
772+
delimiter = escapeString(" ");
773+
} else if ("tsv".equals(collectionFormat)) {
774+
delimiter = escapeString("\t");
775+
} else if ("pipes".equals(collectionFormat)) {
776+
delimiter = escapeString("|");
777+
}
778+
779+
StringBuilder sb = new StringBuilder();
780+
boolean first = true;
781+
for (Object item : value) {
782+
if (!first) {
783+
sb.append(delimiter);
784+
}
785+
sb.append(escapeString(parameterToString(item)));
786+
first = false;
787+
}
788+
return sb.toString();
789+
}
790+
756791
/**
757792
* Serialize the given Java object into string entity according the given
758793
* Content-Type (only JSON is supported for now).

‎samples/client/petstore/java/jersey2-java8-localdatetime/src/main/java/org/openapitools/client/ApiClient.java‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,41 @@ public String escapeString(String str) {
894894
}
895895
}
896896

897+
/**
898+
* Format the given collection as a path parameter value according to the collection format.
899+
* Each item is escaped individually, so that the delimiter is preserved (e.g. "a,b,c" for csv).
900+
*
901+
* @param collectionFormat Collection format (csv by default)
902+
* @param value Collection value
903+
* @return Escaped path parameter value
904+
*/
905+
public String collectionPathParameterToString(String collectionFormat, Collection<?> value) {
906+
if (value == null || value.isEmpty()) {
907+
return "";
908+
}
909+
910+
// "multi" is not valid for path params, fall back to csv
911+
String delimiter = ",";
912+
if ("ssv".equals(collectionFormat)) {
913+
delimiter = escapeString(" ");
914+
} else if ("tsv".equals(collectionFormat)) {
915+
delimiter = escapeString("\t");
916+
} else if ("pipes".equals(collectionFormat)) {
917+
delimiter = escapeString("|");
918+
}
919+
920+
StringBuilder sb = new StringBuilder();
921+
boolean first = true;
922+
for (Object item : value) {
923+
if (!first) {
924+
sb.append(delimiter);
925+
}
926+
sb.append(escapeString(parameterToString(item)));
927+
first = false;
928+
}
929+
return sb.toString();
930+
}
931+
897932
/**
898933
* Serialize the given Java object into string entity according the given
899934
* Content-Type (only JSON is supported for now).

‎samples/client/petstore/java/jersey2-java8/src/main/java/org/openapitools/client/ApiClient.java‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,41 @@ public String escapeString(String str) {
894894
}
895895
}
896896

897+
/**
898+
* Format the given collection as a path parameter value according to the collection format.
899+
* Each item is escaped individually, so that the delimiter is preserved (e.g. "a,b,c" for csv).
900+
*
901+
* @param collectionFormat Collection format (csv by default)
902+
* @param value Collection value
903+
* @return Escaped path parameter value
904+
*/
905+
public String collectionPathParameterToString(String collectionFormat, Collection<?> value) {
906+
if (value == null || value.isEmpty()) {
907+
return "";
908+
}
909+
910+
// "multi" is not valid for path params, fall back to csv
911+
String delimiter = ",";
912+
if ("ssv".equals(collectionFormat)) {
913+
delimiter = escapeString(" ");
914+
} else if ("tsv".equals(collectionFormat)) {
915+
delimiter = escapeString("\t");
916+
} else if ("pipes".equals(collectionFormat)) {
917+
delimiter = escapeString("|");
918+
}
919+
920+
StringBuilder sb = new StringBuilder();
921+
boolean first = true;
922+
for (Object item : value) {
923+
if (!first) {
924+
sb.append(delimiter);
925+
}
926+
sb.append(escapeString(parameterToString(item)));
927+
first = false;
928+
}
929+
return sb.toString();
930+
}
931+
897932
/**
898933
* Serialize the given Java object into string entity according the given
899934
* Content-Type (only JSON is supported for now).

0 commit comments

Comments
 (0)