3

I use this code to perform a HTTP POST request and deserialize the returned value:

ParameterizedTypeReference<MyClass> typeRef = new ParameterizedTypeReference<>() {};
HttpEntity<Object> requestEntity = new HttpEntity<>("some text");
ResponseEntity<MyClass> result = restTemplate.exchange("/test", HttpMethod.POST, requestEntity, typeRef);
MyClass returnValue = result.getBody();

To make it easier to use, I tried to wrap the code in a function like so:

public <T> T post(Object content, Class<T> returnType, String url){
    ParameterizedTypeReference<T> typeRef = new ParameterizedTypeReference<>() {};
    HttpEntity<Object> requestEntity = new HttpEntity<>(content);
    ResponseEntity<T> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, typeRef);
    return response.getBody();
}

However the code stops functioning when it's put in a function. It throws java.lang.ClassCastException: java.base/java.util.LinkedHashMap cannot be cast to client.rest.MyClass. It seems as though some type information is lost somewhere along the way.

Here's the complete code in form of 2 test cases:

package client.rest;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.support.RestGatewaySupport;

import static org.springframework.test.web.client.ExpectedCount.times;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

class MyClass {
    public int getInt(){
        return 1;
    }

    public void setInt(int i){}
}

public class TEMP {

    public static RestTemplate restTemplate = new RestTemplate();
    public static MockRestServiceServer mockServer;

    @BeforeClass
    public static void beforeClass() throws JsonProcessingException {
        MyClass value = new MyClass();

        // set up a mock server
        RestGatewaySupport gateway = new RestGatewaySupport();
        gateway.setRestTemplate(restTemplate);
        mockServer = MockRestServiceServer.bindTo(gateway).build();

        ObjectMapper objectmapper = new ObjectMapper();
        String payload = objectmapper.writeValueAsString(value);

        mockServer.expect(times(2), requestTo("/test"))
            .andRespond(withSuccess(payload, MediaType.APPLICATION_JSON));
    }

    @Test
    public void without_function() {
        ParameterizedTypeReference<MyClass> typeRef = new ParameterizedTypeReference<>() {};
        HttpEntity<Object> requestEntity = new HttpEntity<>("some text");
        ResponseEntity<MyClass> result = restTemplate.exchange("/test", HttpMethod.POST, requestEntity, typeRef);
        MyClass returnValue = result.getBody();
    }

    public <T> T post(Object content, Class<T> returnType, String url){
        ParameterizedTypeReference<T> typeRef = new ParameterizedTypeReference<>() {};
        HttpEntity<Object> requestEntity = new HttpEntity<>(content);
        ResponseEntity<T> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, typeRef);
        return response.getBody();
    }

    @Test
    public void with_function() {
        MyClass returnValue = post("some text", MyClass.class, "/test");
    }
}

My question is twofold:

  1. Why doesn't the function work?
  2. How can I make it work?
Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • According to your exception, your map has at least one element which cannot be cast to your Class.That's why we use generics, for changing the runtime error to the compile time and type safety. – meditat Dec 26 '17 at 06:32
  • 1
    You never use the `returnType`argument. You need to create a ParameterizedTypeReference for that type. https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/core/ParameterizedTypeReference.html#forType-java.lang.reflect.Type- – JB Nizet Dec 26 '17 at 06:51
  • @JBNizet instead of typeRef passing the returnType to exchange method already works, but i guess the main question is ; why ParameterizedTypeReference is not recognizing generics. – Ömer Erden Dec 26 '17 at 07:22
  • @JBNizet I don't understand. Why do I have to use `returnType`, why isn't `T` sufficient? They're pretty much the same thing, no? Could you explain in more detail? – Aran-Fey Dec 26 '17 at 10:41
  • 1
    Because generic types are erased in Java. The whole point of ParameterizedTypeReference is to capture, by subclassing it as an anonymous inner class and instantiating it, the concrete generic type passed when creating it: `new ParameterizedTypeReference() {}`. Once you make this generic, there is no concrete class to be captured. T is erased by the compiler to Object. So Jackson doesn't know which type it must use as a result of the JSON unmarshalling anymore. – JB Nizet Dec 26 '17 at 10:56
  • Closely related question with a lot of useful info: https://stackoverflow.com/questions/21987295/using-spring-resttemplate-in-generic-method-with-generic-parameter (Thanks to shmosel for finding it!) – Aran-Fey Dec 26 '17 at 22:43

2 Answers2

5

Answer for 1.

ParameterizedTypeReference<X> typeRef = new ParameterizedTypeReference<X>() {};

Thanks the final {} jackson is able to find out the what X is in run-time using reflection however X is resolved in compilation time so if you have MyClass or T that is exactly what it will get in runtime; It won't be able to figure out what the T is assigned to in runtime.

For the very same reason, if you keep using the function-less option but you remove the {} at the end it will compile but it will result in the same error.

Answer for 2.

Instead of Class<T> returnType, that you never make reference to btw, you could pass ParameterizedTypeReference<T> typeRef directly. The code that call the post would then need to determine T in compilation time:

@Test
    public void with_function() {
        ParameterizedTypeReference<MyClass> typeRef = new ParameterizedTypeReference<>() {}; 
        MyClass returnValue = post("some text", typeRef, "/test");
    }
}

However I think you should consider alternatives that do not rely on the {} trick which might be problematic.

Have you tried ParameterizedTypeReference's forType?:

public <T> T post(Object content, Class<T> returnType, String url){
        ParameterizedTypeReference<T> typeRef = ParameterizedTypeReference.forType(returnType);
        HttpEntity<Object> requestEntity = new HttpEntity<>(content);
        ResponseEntity<T> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, typeRef);
        return response.getBody();
}

In any case this will work with non-generic assignations to T like MyClass as when passing MyClass.class as the return type; It would not work with ArrayList<MyClass> list; list.getClass() since it would be equivalent to return ArrayList.class. I guess in those cases you would need to construct and pass a different Type instance that would correspond to the more complex type expression.

Valentin Ruano
  • 2,726
  • 19
  • 29
  • 1
    Thanks for the answer. May I suggest that you also submit this answer in the marked duplicate question? None of the answers there mention the `ParameterizedTypeReference.forType` function, so your answer would be very valuable there. – Aran-Fey Dec 26 '17 at 21:05
  • @Rawing Have you verified that it works? Happy to do if it does. – Valentin Ruano Dec 26 '17 at 21:08
  • 1
    @Rawing That's because this only works for a non-parameterized type, where you don't actually need `ParameterizedTypeReference` in the first place. Reopening the question because of the distinction. – shmosel Dec 26 '17 at 21:12
0

You don't need a ParameterizedTypeReference because you don't have a parameterized type. Just use the overload that accepts a Class directly:

ResponseEntity<T> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, returnType);
shmosel
  • 49,289
  • 6
  • 73
  • 138
  • Ah, you're right of course. The reason why I was using a ParameterizedTypeReference was because I actually had 2 `post` functions - 1 with a `Class` parameter as seen in the question, and 1 with a `ParameterizedTypeReference` parameter. The one function turns the `Class` into a ParameterizedTypeReference and passes it to the other function. I merged the two functions into a single one for the purpose of the question, sorry about that! – Aran-Fey Dec 29 '17 at 18:03