diff --git a/.github/workflows/test-java-feign-jackson-headers.yaml b/.github/workflows/test-java-feign-jackson-headers.yaml new file mode 100644 index 000000000000..9b1c7d1ef05e --- /dev/null +++ b/.github/workflows/test-java-feign-jackson-headers.yaml @@ -0,0 +1,32 @@ +name: Java Feign Jackson headers +on: + push: + paths: + - 'modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java' + - 'modules/openapi-generator/src/main/resources/Java/**' + - 'modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-json-header.yaml' + - 'modules/openapi-generator/src/test/resources/3_1/java/FeignJacksonHeaderRuntimeTest.java' + - '.github/workflows/test-java-feign-jackson-headers.yaml' + pull_request: + paths: + - 'modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java' + - 'modules/openapi-generator/src/main/resources/Java/**' + - 'modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-json-header.yaml' + - 'modules/openapi-generator/src/test/resources/3_1/java/FeignJacksonHeaderRuntimeTest.java' + - '.github/workflows/test-java-feign-jackson-headers.yaml' +jobs: + runtime: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 + with: + distribution: temurin + java-version: '17' + cache: maven + - run: ./mvnw -B -pl modules/openapi-generator-cli -am -DskipTests package + - run: | + java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java --library feign -i modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-json-header.yaml -o /tmp/feign-test --additional-properties=serializationLibrary=jackson --global-property=apis,models,supportingFiles,apiTests=false,modelTests=false + mkdir -p /tmp/feign-test/src/test/java + cp modules/openapi-generator/src/test/resources/3_1/java/FeignJacksonHeaderRuntimeTest.java /tmp/feign-test/src/test/java/ + mvn -B -f /tmp/feign-test/pom.xml -Dtest=FeignJacksonHeaderRuntimeTest test diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java index c2b1a9aa93b0..dc4ed3d3b8aa 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java @@ -17,6 +17,7 @@ package org.openapitools.codegen.languages; +import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.servers.Server; @@ -947,6 +948,17 @@ public void processOpts() { public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { super.postProcessOperationsWithModels(objs, allModels); + if (isLibrary(FEIGN) && jackson && objs.getOperations() != null) { + for (CodegenOperation operation : objs.getOperations().getOperation()) { + if (operation.allParams.stream().anyMatch(p -> p.isHeaderParam && p.headerIsJsonMimeType)) { + additionalProperties.put("hasFeignJsonHeader", true); + if (operation.allParams.stream().anyMatch(p -> p.isHeaderParam && p.headerIsJsonMimeType && p.isArray)) { + additionalProperties.put("hasFeignJsonHeaderArrays", true); + } + } + } + } + if (this.getSingleRequestParameter() && (isLibrary(JERSEY2) || isLibrary(JERSEY3) || isLibrary(OKHTTP_GSON) || isLibrary(NATIVE))) { // loop through operations to set x-group-parameters extension to true if useSingleRequestParameter option is enabled OperationMap operations = objs.getOperations(); @@ -1141,6 +1153,13 @@ private static boolean isMultipartType(List> consumes) { return false; } + @Override + public void processOpenAPI(OpenAPI openAPI) { + super.processOpenAPI(openAPI); + additionalProperties.remove("hasFeignJsonHeader"); + additionalProperties.remove("hasFeignJsonHeaderArrays"); + } + @Override public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { super.postProcessModelProperty(model, property); diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/feign/ApiClient.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/feign/ApiClient.mustache index 3e8a99abd201..57523f7b1ffd 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/feign/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/feign/ApiClient.mustache @@ -61,6 +61,103 @@ import feign.Retryer; {{>generatedAnnotation}} public class ApiClient { + {{#hasFeignJsonHeader}} + /** Serializes JSON-content headers with the client's Jackson configuration. */ + public static class JsonHeaderParamExpander implements feign.Param.Expander { + private final ApiClient client; + + public JsonHeaderParamExpander() { + throw new IllegalStateException("JSON header expander requires a configured ApiClient contract"); + } + + private JsonHeaderParamExpander(ApiClient client) { + this.client = client; + } + + @Override + public String expand(Object value) { + try { + if (value instanceof JsonHeaderIterable) value = ((JsonHeaderIterable) value).value; + return client.objectMapper.copy() + .setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL) + .writer() + .with(com.fasterxml.jackson.core.JsonGenerator.Feature.ESCAPE_NON_ASCII) + .writeValueAsString(value); + } catch (com.fasterxml.jackson.core.JsonProcessingException e) { + throw new IllegalArgumentException("Failed to serialize JSON header", e); + } + } + } + + private static final class JsonHeaderIterable { + private final Object value; + private JsonHeaderIterable(Object value) { this.value = value; } + } + + {{#hasFeignJsonHeaderArrays}} + private static final class JsonHeaderInvocationHandler implements java.lang.reflect.InvocationHandler { + private final Object target; + private final java.lang.reflect.InvocationHandler delegate; + + private JsonHeaderInvocationHandler(Object target) { + this.target = target; + this.delegate = java.lang.reflect.Proxy.getInvocationHandler(target); + } + + @Override + public Object invoke(Object proxy, java.lang.reflect.Method method, Object[] args) throws Throwable { + if (method.getDeclaringClass() == Object.class && method.getName().equals("equals")) { + Object other = args[0]; + if (other == proxy) return true; + if (other != null && java.lang.reflect.Proxy.isProxyClass(other.getClass())) { + java.lang.reflect.InvocationHandler otherHandler = java.lang.reflect.Proxy.getInvocationHandler(other); + if (otherHandler instanceof JsonHeaderInvocationHandler) { + return delegate.equals(((JsonHeaderInvocationHandler) otherHandler).delegate); + } + } + return false; + } + if (args != null) { + args = args.clone(); + java.lang.annotation.Annotation[][] annotations = method.getParameterAnnotations(); + for (int i = 0; i < args.length; i++) { + if (!(args[i] instanceof Iterable)) continue; + for (java.lang.annotation.Annotation annotation : annotations[i]) { + if (annotation instanceof feign.Param + && ((feign.Param) annotation).expander() == JsonHeaderParamExpander.class) { + args[i] = new JsonHeaderIterable(args[i]); + break; + } + } + } + } + return delegate.invoke(target, method, args); + } + } + {{/hasFeignJsonHeaderArrays}} + + private feign.Contract jsonHeaderContract() { + feign.Contract delegate = new feign.Contract.Default(); + return type -> { + java.util.List methods = delegate.parseAndValidateMetadata(type); + for (feign.MethodMetadata method : methods) { + java.util.Map expanders = new java.util.LinkedHashMap<>(); + if (method.indexToExpander() != null) expanders.putAll(method.indexToExpander()); + java.util.Iterator>> entries = + method.indexToExpanderClass().entrySet().iterator(); + while (entries.hasNext()) { + java.util.Map.Entry> entry = entries.next(); + if (entry.getValue() == JsonHeaderParamExpander.class) { + expanders.put(entry.getKey(), new JsonHeaderParamExpander(this)); + entries.remove(); + } + } + method.indexToExpander(expanders); + } + return methods; + }; + } + {{/hasFeignJsonHeader}} protected static final Logger log = Logger.getLogger(ApiClient.class.getName()); public interface Api {} @@ -89,6 +186,9 @@ public class ApiClient { {{#feign-hc5}} .client(APACHE_HTTP5_CLIENT) {{/feign-hc5}} + {{#hasFeignJsonHeader}} + .contract(jsonHeaderContract()) + {{/hasFeignJsonHeader}} .encoder(new FormEncoder(new JacksonEncoder(objectMapper))) .decoder(new ApiResponseDecoder(objectMapper)) {{#hasOAuthMethods}} @@ -188,6 +288,9 @@ public class ApiClient { public ApiClient setFeignBuilder(Feign.Builder feignBuilder) { this.feignBuilder = feignBuilder; + {{#hasFeignJsonHeader}} + this.feignBuilder.contract(jsonHeaderContract()); + {{/hasFeignJsonHeader}} return this; } @@ -255,7 +358,14 @@ public class ApiClient { * @return The Client */ public T buildClient(Class clientClass) { + {{#hasFeignJsonHeaderArrays}} + T target = feignBuilder.target(clientClass, basePath); + return clientClass.cast(java.lang.reflect.Proxy.newProxyInstance(clientClass.getClassLoader(), + new Class[] { clientClass }, new JsonHeaderInvocationHandler(target))); + {{/hasFeignJsonHeaderArrays}} + {{^hasFeignJsonHeaderArrays}} return feignBuilder.target(clientClass, basePath); + {{/hasFeignJsonHeaderArrays}} } /** diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/feign/api.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/feign/api.mustache index fd660d4c5510..3559c47fac03 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/feign/api.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/feign/api.mustache @@ -53,7 +53,7 @@ public interface {{classname}} extends ApiClient.Api { "{{baseName}}: {{=<% %>=}}{<%paramName%>}<%={{ }}=%>"{{^-last}}, {{/-last}}{{/headerParams}} }) - {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{nickname}}({{#allParams}}{{^isBodyParam}}{{^isFormParam}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{nickname}}({{#allParams}}{{^isBodyParam}}{{^isFormParam}}{{#jackson}}{{#headerIsJsonMimeType}}@Param(value="{{paramName}}", expander=ApiClient.JsonHeaderParamExpander.class) {{/headerIsJsonMimeType}}{{/jackson}}{{^headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{^jackson}}{{#headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{/jackson}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); /** * {{summary}} @@ -83,7 +83,7 @@ public interface {{classname}} extends ApiClient.Api { "{{baseName}}: {{=<% %>=}}{<%paramName%>}<%={{ }}=%>"{{^-last}}, {{/-last}}{{/headerParams}} }) - ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{nickname}}WithHttpInfo({{#allParams}}{{^isBodyParam}}{{^isFormParam}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{nickname}}WithHttpInfo({{#allParams}}{{^isBodyParam}}{{^isFormParam}}{{#jackson}}{{#headerIsJsonMimeType}}@Param(value="{{paramName}}", expander=ApiClient.JsonHeaderParamExpander.class) {{/headerIsJsonMimeType}}{{/jackson}}{{^headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{^jackson}}{{#headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{/jackson}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); {{#hasQueryParams}} @@ -128,7 +128,7 @@ public interface {{classname}} extends ApiClient.Api { "{{baseName}}: {{=<% %>=}}{<%paramName%>}<%={{ }}=%>"{{^-last}}, {{/-last}}{{/headerParams}} }) - {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{nickname}}({{#allParams}}{{^isQueryParam}}{{^isBodyParam}}{{^isFormParam}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}, {{/isQueryParam}}{{/allParams}}@QueryMap {{operationIdCamelCase}}QueryParams queryParams); + {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{nickname}}({{#allParams}}{{^isQueryParam}}{{^isBodyParam}}{{^isFormParam}}{{#jackson}}{{#headerIsJsonMimeType}}@Param(value="{{paramName}}", expander=ApiClient.JsonHeaderParamExpander.class) {{/headerIsJsonMimeType}}{{/jackson}}{{^headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{^jackson}}{{#headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{/jackson}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}, {{/isQueryParam}}{{/allParams}}@QueryMap {{operationIdCamelCase}}QueryParams queryParams); /** * {{summary}} @@ -168,7 +168,7 @@ public interface {{classname}} extends ApiClient.Api { "{{baseName}}: {{=<% %>=}}{<%paramName%>}<%={{ }}=%>"{{^-last}}, {{/-last}}{{/headerParams}} }) - ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{nickname}}WithHttpInfo({{#allParams}}{{^isQueryParam}}{{^isBodyParam}}{{^isFormParam}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}, {{/isQueryParam}}{{/allParams}}@QueryMap {{operationIdCamelCase}}QueryParams queryParams); + ApiResponse<{{#returnType}}{{{.}}}{{/returnType}}{{^returnType}}Void{{/returnType}}> {{nickname}}WithHttpInfo({{#allParams}}{{^isQueryParam}}{{^isBodyParam}}{{^isFormParam}}{{#jackson}}{{#headerIsJsonMimeType}}@Param(value="{{paramName}}", expander=ApiClient.JsonHeaderParamExpander.class) {{/headerIsJsonMimeType}}{{/jackson}}{{^headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{^jackson}}{{#headerIsJsonMimeType}}{{^legacyDates}}@Param("{{paramName}}") {{/legacyDates}}{{#legacyDates}}@Param(value="{{paramName}}", expander=ParamExpander.class) {{/legacyDates}}{{/headerIsJsonMimeType}}{{/jackson}}{{/isFormParam}}{{#isFormParam}}@Param("{{baseName}}") {{/isFormParam}}{{/isBodyParam}}{{>nullable_var_annotations}} {{{dataType}}} {{paramName}}, {{/isQueryParam}}{{/allParams}}@QueryMap {{operationIdCamelCase}}QueryParams queryParams); /** diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index 435767b783ec..522f49f42381 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -81,6 +81,39 @@ public class JavaClientCodegenTest { private static final String JAVA_GENERATOR = "java"; + @Test + public void testFeignJacksonJsonHeader() { + final Path output = newTempFolder(); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName(JAVA_GENERATOR) + .setLibrary(FEIGN) + .addAdditionalProperty("serializationLibrary", "jackson") + .setInputSpec("src/test/resources/3_1/java/feign-jackson-json-header.yaml") + .setOutputDir(output.toString()); + List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + validateJavaSourceFiles(files); + Path java = output.resolve("src/main/java/org/openapitools/client"); + assertThat(java.resolve("api/DefaultApi.java")).content() + .contains("@Param(value=\"xJsonArg\", expander=ApiClient.JsonHeaderParamExpander.class)", + "@Param(\"xPlain\")"); + assertThat(java.resolve("ApiClient.java")).content() + .contains("class JsonHeaderParamExpander", "ESCAPE_NON_ASCII", "writeValueAsString(value)"); + } + + @Test + public void testFeignJacksonResponseHeaderDoesNotEnableRequestExpander() { + Path output = newTempFolder(); + CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName(JAVA_GENERATOR) + .setLibrary(FEIGN) + .addAdditionalProperty("serializationLibrary", "jackson") + .setInputSpec("src/test/resources/3_1/java/feign-jackson-response-header.yaml") + .setOutputDir(output.toString()); + new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + assertThat(output.resolve("src/main/java/org/openapitools/client/ApiClient.java")) + .content().doesNotContain("JsonHeaderParamExpander"); + } + // This is the kind of information that ideally would be defined and available system-wide @Getter enum Library { diff --git a/modules/openapi-generator/src/test/resources/3_1/java/FeignJacksonHeaderRuntimeTest.java b/modules/openapi-generator/src/test/resources/3_1/java/FeignJacksonHeaderRuntimeTest.java new file mode 100644 index 000000000000..7f821ebc5026 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/java/FeignJacksonHeaderRuntimeTest.java @@ -0,0 +1,103 @@ +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.Response; +import org.junit.jupiter.api.Test; +import org.openapitools.client.ApiClient; +import org.openapitools.client.api.DefaultApi; +import org.openapitools.client.model.HeaderArg; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import static org.junit.jupiter.api.Assertions.*; + +class FeignJacksonHeaderRuntimeTest { + @Test + void serializesJsonHeadersAndPreservesPlainHeaders() { + ApiClient client = new ApiClient(); + client.getFeignBuilder().client((request, options) -> { + assertEquals(Collections.singletonList("{\"path\":\"/\\u0442\\u0435\\u0441\\u0442\\uD83D\\uDE00\"}"), + request.headers().get("X-Json-Arg")); + assertEquals(Collections.singletonList("plain"), request.headers().get("X-Plain")); + return Response.builder().request(request).status(204).reason("No Content") + .headers(Collections.emptyMap()) + .build(); + }); + client.buildClient(DefaultApi.class).getMetadata(new HeaderArg().path("/ั‚ะตัั‚๐Ÿ˜€"), "plain"); + } + + @Test + void usesConfiguredMapperForJsonHeaders() { + ApiClient client = new ApiClient(); + ObjectMapper customMapper = client.getObjectMapper().copy(); + com.fasterxml.jackson.databind.module.SimpleModule module = new com.fasterxml.jackson.databind.module.SimpleModule(); + module.addSerializer(HeaderArg.class, new com.fasterxml.jackson.databind.JsonSerializer() { + @Override + public void serialize(HeaderArg value, com.fasterxml.jackson.core.JsonGenerator generator, + com.fasterxml.jackson.databind.SerializerProvider provider) throws java.io.IOException { + generator.writeStartObject(); + generator.writeStringField("custom", value.getPath()); + generator.writeEndObject(); + } + }); + customMapper.registerModule(module); + client.setObjectMapper(customMapper); + client.getFeignBuilder().client((request, options) -> { + assertEquals(Collections.singletonList("{\"custom\":\"/x\"}"), request.headers().get("X-Json-Arg")); + return Response.builder().request(request).status(204).reason("No Content").headers(Collections.emptyMap()) + .build(); + }); + client.buildClient(DefaultApi.class).getMetadata(new HeaderArg().path("/x"), "plain"); + } + + @Test + void serializesJsonArrayAsOneHeader() { + ApiClient client = new ApiClient(); + client.getFeignBuilder().client((request, options) -> { + assertEquals(Collections.singletonList("[{\"path\":\"/a\"},{\"path\":\"/b\"}]"), + request.headers().get("X-Json-Array")); + return Response.builder().request(request).status(204).reason("No Content") + .headers(Collections.emptyMap()).build(); + }); + client.buildClient(DefaultApi.class).getArray(java.util.Arrays.asList(new HeaderArg().path("/a"), new HeaderArg().path("/b"))); + } + + @Test + void replacingBuilderKeepsClientMapper() { + ApiClient client = new ApiClient(); + com.fasterxml.jackson.databind.module.SimpleModule module = new com.fasterxml.jackson.databind.module.SimpleModule(); + module.addSerializer(HeaderArg.class, new com.fasterxml.jackson.databind.JsonSerializer() { + @Override + public void serialize(HeaderArg value, com.fasterxml.jackson.core.JsonGenerator generator, + com.fasterxml.jackson.databind.SerializerProvider provider) throws java.io.IOException { + generator.writeString("custom"); + } + }); + client.getObjectMapper().registerModule(module); + client.setFeignBuilder(feign.Feign.builder().client((request, options) -> { + assertEquals(Collections.singletonList("\"custom\""), request.headers().get("X-Json-Arg")); + return Response.builder().request(request).status(204).reason("No Content").headers(Collections.emptyMap()).build(); + })); + client.buildClient(DefaultApi.class).getMetadata(new HeaderArg().path("/x").optionalValue("v"), "plain"); + } + + @Test + void requestBodyMapperRetainsNullBehavior() throws Exception { + String json = new ApiClient().getObjectMapper().writeValueAsString(new HeaderArg().path("/x")); + assertTrue(json.contains("\"optionalValue\":null")); + } + + @Test + void serializesEmptyJsonArray() { + ApiClient client = new ApiClient(); + client.getFeignBuilder().client((request, options) -> { + assertEquals(Collections.singletonList("[]"), request.headers().get("X-Json-Array")); + return Response.builder().request(request).status(204).reason("No Content") + .headers(Collections.emptyMap()).build(); + }); + DefaultApi api = client.buildClient(DefaultApi.class); + assertEquals(api, api); + DefaultApi other = client.buildClient(DefaultApi.class); + assertEquals(api, other); + assertEquals(api.hashCode(), other.hashCode()); + api.getArray(Collections.emptyList()); + } + +} diff --git a/modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-json-header.yaml b/modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-json-header.yaml new file mode 100644 index 000000000000..e28aa9692965 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-json-header.yaml @@ -0,0 +1,48 @@ +openapi: 3.1.0 +info: + title: Feign JSON header + version: 1.0.0 +paths: + /array: + get: + operationId: getArray + parameters: + - name: X-Json-Array + in: header + required: true + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/HeaderArg' + responses: + '204': + description: Success + /metadata: + get: + operationId: getMetadata + parameters: + - name: X-Json-Arg + in: header + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/HeaderArg' + - name: X-Plain + in: header + schema: + type: string + responses: + '204': + description: Success +components: + schemas: + HeaderArg: + type: object + properties: + path: + type: string + optionalValue: + type: string diff --git a/modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-response-header.yaml b/modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-response-header.yaml new file mode 100644 index 000000000000..81c80aefe396 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_1/java/feign-jackson-response-header.yaml @@ -0,0 +1,13 @@ +openapi: 3.1.0 +info: {title: Response header, version: '1'} +paths: + /test: + get: + responses: + '204': + description: ok + headers: + X-Json: + content: + application/json: + schema: {type: object}