Repository: quarkusio/quarkus
Issue: REST Client doesn't register Interface return types for reflection

Currently the REST Client extension doesn't register the return types of the methods used in a `@RegisterRESTClient` annotated interface for reflection.

This will result in those classes being DCE'd for native image builds

Changed files:
extensions/smallrye-rest-client/deployment/src/main/java/io/quarkus/smallrye/restclient/deployment/SmallRyeRestClientProcessor.java
integration-tests/main/src/main/java/io/quarkus/example/rest/RestInterface.java
integration-tests/main/src/main/java/io/quarkus/example/rest/ClientResource.java
integration-tests/main/src/test/java/io/quarkus/example/test/RestClientTestCase.java

diff --git a/extensions/smallrye-rest-client/deployment/src/main/java/io/quarkus/smallrye/restclient/deployment/SmallRyeRestClientProcessor.java b/extensions/smallrye-rest-client/deployment/src/main/java/io/quarkus/smallrye/restclient/deployment/SmallRyeRestClientProcessor.java
index d3f97478254..0ce7c5be365 100644
--- a/extensions/smallrye-rest-client/deployment/src/main/java/io/quarkus/smallrye/restclient/deployment/SmallRyeRestClientProcessor.java
+++ b/extensions/smallrye-rest-client/deployment/src/main/java/io/quarkus/smallrye/restclient/deployment/SmallRyeRestClientProcessor.java
@@ -18,7 +18,9 @@
 
 import java.lang.reflect.Modifier;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 import javax.ws.rs.client.ClientRequestFilter;
 import javax.ws.rs.client.ClientResponseFilter;
@@ -32,6 +34,8 @@
 import org.jboss.jandex.AnnotationTarget;
 import org.jboss.jandex.ClassInfo;
 import org.jboss.jandex.DotName;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.Type;
 import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
 import org.jboss.resteasy.client.jaxrs.internal.proxy.ProxyBuilderImpl;
 import org.jboss.resteasy.client.jaxrs.internal.proxy.ResteasyClientProxy;
@@ -52,6 +56,7 @@
 import io.quarkus.deployment.builditem.FeatureBuildItem;
 import io.quarkus.deployment.builditem.SslNativeConfigBuildItem;
 import io.quarkus.deployment.builditem.substrate.ReflectiveClassBuildItem;
+import io.quarkus.deployment.builditem.substrate.ReflectiveHierarchyBuildItem;
 import io.quarkus.deployment.builditem.substrate.SubstrateProxyDefinitionBuildItem;
 import io.quarkus.deployment.builditem.substrate.SubstrateResourceBuildItem;
 import io.quarkus.deployment.util.ServiceUtil;
@@ -127,11 +132,13 @@ void processInterfaces(CombinedIndexBuildItem combinedIndexBuildItem,
             SslNativeConfigBuildItem sslNativeConfig,
             BuildProducer<SubstrateProxyDefinitionBuildItem> proxyDefinition,
             BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
+            BuildProducer<ReflectiveHierarchyBuildItem> reflectiveHierarchy,
             BuildProducer<BeanRegistrarBuildItem> beanRegistrars,
             BuildProducer<ExtensionSslNativeSupportBuildItem> extensionSslNativeSupport) {
 
         // According to the spec only rest client interfaces annotated with RegisterRestClient are registered as beans
         Map<DotName, ClassInfo> interfaces = new HashMap<>();
+        Set<Type> returnTypes = new HashSet<>();
 
         for (AnnotationInstance annotation : combinedIndexBuildItem.getIndex().getAnnotations(REGISTER_REST_CLIENT)) {
             AnnotationTarget target = annotation.target();
@@ -147,6 +154,16 @@ void processInterfaces(CombinedIndexBuildItem combinedIndexBuildItem,
                 continue;
             }
             interfaces.put(theInfo.name(), theInfo);
+
+            // Find Return types
+            for (MethodInfo method : theInfo.methods()) {
+                Type type = method.returnType();
+                if (!type.name().toString().contains("java.lang")) {
+                    if (!returnTypes.contains(type)) {
+                        returnTypes.add(type);
+                    }
+                }
+            }
         }
 
         if (interfaces.isEmpty()) {
@@ -160,6 +177,11 @@ void processInterfaces(CombinedIndexBuildItem combinedIndexBuildItem,
             reflectiveClass.produce(new ReflectiveClassBuildItem(true, false, iName));
         }
 
+        // Register Interface return types for reflection
+        for (Type returnType : returnTypes) {
+            reflectiveHierarchy.produce(new ReflectiveHierarchyBuildItem(returnType));
+        }
+
         beanRegistrars.produce(new BeanRegistrarBuildItem(new BeanRegistrar() {
 
             @Override
diff --git a/integration-tests/main/src/main/java/io/quarkus/example/rest/ClientResource.java b/integration-tests/main/src/main/java/io/quarkus/example/rest/ClientResource.java
index 9860261e6a5..a45baa50a43 100644
--- a/integration-tests/main/src/main/java/io/quarkus/example/rest/ClientResource.java
+++ b/integration-tests/main/src/main/java/io/quarkus/example/rest/ClientResource.java
@@ -17,10 +17,12 @@
 package io.quarkus.example.rest;
 
 import java.net.URL;
+import java.util.List;
 
 import javax.inject.Inject;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
 
 import org.eclipse.microprofile.rest.client.RestClientBuilder;
 import org.eclipse.microprofile.rest.client.inject.RestClient;
@@ -47,4 +49,40 @@ public String cdi() throws Exception {
         return restInterface.get();
     }
 
+    @GET
+    @Path("manual/jackson")
+    @Produces("application/json")
+    public TestResource.MyData getDataManual() throws Exception {
+        RestInterface iface = RestClientBuilder.newBuilder()
+                .baseUrl(new URL(System.getProperty("test.url")))
+                .build(RestInterface.class);
+        System.out.println(iface.getData());
+        return iface.getData();
+    }
+
+    @GET
+    @Path("cdi/jackson")
+    @Produces("application/json")
+    public TestResource.MyData getDataCdi() {
+        return restInterface.getData();
+    }
+
+    @GET
+    @Path("/manual/complex")
+    @Produces("application/json")
+    public List<ComponentType> complexManual() throws Exception {
+        RestInterface iface = RestClientBuilder.newBuilder()
+                .baseUrl(new URL(System.getProperty("test.url")))
+                .build(RestInterface.class);
+        System.out.println(iface.complex());
+        return iface.complex();
+    }
+
+    @GET
+    @Path("/cdi/complex")
+    @Produces("application/json")
+    public List<ComponentType> complexCdi() {
+        return restInterface.complex();
+    }
+
 }
diff --git a/integration-tests/main/src/main/java/io/quarkus/example/rest/RestInterface.java b/integration-tests/main/src/main/java/io/quarkus/example/rest/RestInterface.java
index 5e9c3edfd3a..25a3fae1d89 100644
--- a/integration-tests/main/src/main/java/io/quarkus/example/rest/RestInterface.java
+++ b/integration-tests/main/src/main/java/io/quarkus/example/rest/RestInterface.java
@@ -16,8 +16,11 @@
 
 package io.quarkus.example.rest;
 
+import java.util.List;
+
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
 
 import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
 
@@ -27,4 +30,14 @@ public interface RestInterface {
 
     @GET
     String get();
+
+    @GET
+    @Path("/jackson")
+    @Produces("application/json")
+    TestResource.MyData getData();
+
+    @GET
+    @Path("/complex")
+    @Produces("application/json")
+    List<ComponentType> complex();
 }
diff --git a/integration-tests/main/src/test/java/io/quarkus/example/test/RestClientTestCase.java b/integration-tests/main/src/test/java/io/quarkus/example/test/RestClientTestCase.java
index 457cacdfd17..430938d1f8e 100644
--- a/integration-tests/main/src/test/java/io/quarkus/example/test/RestClientTestCase.java
+++ b/integration-tests/main/src/test/java/io/quarkus/example/test/RestClientTestCase.java
@@ -19,11 +19,17 @@
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.is;
 
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
+import io.quarkus.example.rest.ComponentType;
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.RestAssured;
+import io.restassured.path.json.JsonPath;
 
 @QuarkusTest
 public class RestClientTestCase {
@@ -40,6 +46,38 @@ public void testMicroprofileClientCDIIntegration() {
                 .body(is("TEST"));
     }
 
+    @Test
+    void testMicroprofileClientData() {
+        JsonPath jsonPath = RestAssured.when().get("/client/manual/jackson").thenReturn().jsonPath();
+        Assertions.assertEquals(jsonPath.getString("name"), "Stuart");
+        Assertions.assertEquals(jsonPath.getString("value"), "A Value");
+    }
+
+    @Test
+    void testMicroprofileClientDataCdi() {
+        JsonPath jsonPath = RestAssured.when().get("/client/cdi/jackson").thenReturn().jsonPath();
+        Assertions.assertEquals(jsonPath.getString("name"), "Stuart");
+        Assertions.assertEquals(jsonPath.getString("value"), "A Value");
+    }
+
+    @Test
+    void testMicroprofileClientComplex() {
+        JsonPath jsonPath = RestAssured.when().get("/client/manual/complex").thenReturn().jsonPath();
+        List<Map<String, String>> components = jsonPath.getList("$");
+        Assertions.assertEquals(components.size(), 1);
+        Map<String, String> map = components.get(0);
+        Assertions.assertEquals(map.get("value"), "component value");
+    }
+
+    @Test
+    void testMicroprofileClientComplexCdi() {
+        JsonPath jsonPath = RestAssured.when().get("/client/cdi/complex").thenReturn().jsonPath();
+        List<Map<String, String>> components = jsonPath.getList("$");
+        Assertions.assertEquals(components.size(), 1);
+        Map<String, String> map = components.get(0);
+        Assertions.assertEquals(map.get("value"), "component value");
+    }
+
     /**
      * Disabled by default as it establishes external connections.
      * <p>