Feign encountered generic type erasure during serialization, causing it to become a LinkedHashMap during deserialization.

Feign encountered generic type erasure during serialization, causing it to become a LinkedHashMap during deserialization

    • Failure background
    • problem analysis
    • Fix
      • Fix 1: Avoid using generics
      • Fix 2: Process when parsing data generics

Fault background

Suppose we have a Feign interface

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;

@FeignClient(name = "testJdkDateTimeRpcService", url = "${query-current-service-provider.prevBaseUrl}")
public interface TestJdkDateTimeRpcService {<!-- -->
    /**
     * Test JDK Date type serialization and deserialization
     * @param testJdkDateTimeParam
     * @return
     */
    @PostMapping(value = "/rpc-service/testJdkDateTimeRpcService/fetchParamIncludeJdkDateTime.do",
            consumes = MediaType.ALL_VALUE,
            produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    VueElementAdminResponse fetchParamIncludeJdkDateTime(@SpringQueryMap TestJdkDateTimeParam testJdkDateTimeParam);
}

The response result object of this interface contains generic types, such as Object,

@Data
public class VueElementAdminResponse implements Serializable {<!-- -->
    private static final long serialVersionUID = -3368531539063907497L;
    private Integer code;
    private String message;
    private Object data;
    private String trackId = SmartStringUtils.getSnowFlakeStrId();
}

There is also a business object

import lombok.Data;

import java.io.Serializable;

@Data
public class TestJdkDateTimeParam implements Serializable {<!-- -->
    private static final long serialVersionUID = -5346602757850243509L;
    private String id;
    private String testDate;
}

Then we defined a Feign calling interface

import org.springframework.web.bind.annotation.*;

@RequestMapping(value = "/rpc-service/testJdkDateTimeRpcService")
@RestController
public class TestJdkDateTimeRpcController {<!-- -->

    @PostMapping(value = "/fetchParamIncludeJdkDateTime.do")
    public VueElementAdminResponse fetchParamIncludeJdkDateTime(@ModelAttribute TestJdkDateTimeParam testJdkDateTimeParam){<!-- -->
        VueElementAdminResponse vueElementAdminResponse=new VueElementAdminResponse();
        vueElementAdminResponse.setCode(20000);
        vueElementAdminResponse.setMessage("/rpc-service/testJdkDateTimeRpcService/fetchParamIncludeJdkDateTime.do-Test JDK DateTime serialization and deserialization when Feign is called");
        vueElementAdminResponse.setData(testJdkDateTimeParam);
        return vueElementAdminResponse;
    }
}

It is worth noting:
We assign a TestJdkDateTimeParam object in the data of the VueElementAdminResponse object.

Finally, define an interface for local calls:

@Slf4j
@RequestMapping(value = "/test-service/testJdkDateTimeRpcService")
@RestController
public class TestJdkDateTimeWebController {<!-- -->

    private final TestJdkDateTimeRpcService testJdkDateTimeRpcService;

    public TestJdkDateTimeWebController(TestJdkDateTimeRpcService testJdkDateTimeRpcService) {<!-- -->
        this.testJdkDateTimeRpcService = testJdkDateTimeRpcService;
    }

    @PostMapping(value = "/testParamIncludeJdkDateTime.do")
    public VueElementAdminResponse fetchParamIncludeJdkDateTime(){<!-- -->
        TestJdkDateTimeParam testJdkDateTimeParam=new TestJdkDateTimeParam();
        testJdkDateTimeParam.setTestDate(DateUtil.formatDate(new Date()));
        testJdkDateTimeParam.setId(SmartStringUtils.getSnowFlakeStrId());
        
        VueElementAdminResponse vueElementAdminResponse=testJdkDateTimeRpcService.fetchParamIncludeJdkDateTime(testJdkDateTimeParam);
        // Incorrect usage will throw an exception java.util.LinkedHashMap cannot be cast to com.xxx.business.test.model.TestJdkDateTimeParam
        TestJdkDateTimeParam testJdkDateTimeParam1= (TestJdkDateTimeParam)vueElementAdminResponse.getData();
        log.info("test:{}",testJdkDateTimeParam1);
        return vueElementAdminResponse;
    }
}

If we try to use the following code

TestJdkDateTimeParam testJdkDateTimeParam1= (TestJdkDateTimeParam)vueElementAdminResponse.getData();

The execution will report the following error:

java.lang.ClassCastException:

java.util.LinkedHashMap cannot be cast to com.xxx.business.test.model.TestJdkDateTimeParam

Problem analysis

Debugging we will find that the TestJdkDateTimeParam object we put in becomes a LinkedHashMap

why?

  • When using Feign to make remote calls, if the request or response contains complex objects (such as custom POJO classes), Feign needs to serialize these objects into a certain format for transmission over the network. By default, Feign uses the Jackson library to handle serialization and deserialization operations.
  • If you observe that Feign serializes the type of Object object into a LinkedHashMap, it may be due to the following reasons:
    • Missing serialization information: When Feign is serializing, it needs to know the type information of the object in order to deserialize it correctly. Without the correct type information, Feign may serialize the object into a LinkedHashMap-like format that preserves some key information (such as field names), but loses the object’s actual type information.
    • Generic type erasure: Java’s generics will be type erased after compilation, which means that the specific type information of the object may be lost at runtime. Feign may encounter erasure of generic types when serializing, resulting in the inability to accurately identify the actual type of the object.

That is to say:

When using Feign to make remote calls, if the request or response contains a complex object (such as a custom POJO class), and this object contains Object generics.

Then Feign will serialize the type of Object object into LinkedHashMap. If you don’t pay attention during parsing, errors similar to the following will often occur

java.lang.ClassCastException:
 java.util.LinkedHashMap cannot be cast to com.xxx.business.test.model.TestJdkDateTimeParam

Repair plan

Repair 1: Avoid using generics

When calling using the Feign interface, avoid using generics, that is, redefine a VO object returned uniformly by Feign:

public class VueElementAdminRpcResponseDTO implements Serializable {<!-- -->
    private static final long serialVersionUID = -3368531539063907497L;
    private Integer code;
    private String message;
    private String data;
    private String trackId = SmartStringUtils.getSnowFlakeStrId();
}

Repair 2: Process when parsing data generics

If you don’t want to change, you can just process it during parsing and deserialization.

The processing ideas when parsing data generics are as follows:

  1. First use json tool to convert string
  2. Then deserialize it into an object

The reference code is as follows:

@Slf4j
@RequestMapping(value = "/test-service/testJdkDateTimeRpcService")
@RestController
public class TestJdkDateTimeWebController {<!-- -->

    private final TestJdkDateTimeRpcService testJdkDateTimeRpcService;

    public TestJdkDateTimeWebController(TestJdkDateTimeRpcService testJdkDateTimeRpcService) {<!-- -->
        this.testJdkDateTimeRpcService = testJdkDateTimeRpcService;
    }

    @PostMapping(value = "/testParamIncludeJdkDateTime.do")
    public VueElementAdminResponse fetchParamIncludeJdkDateTime(){<!-- -->
        TestJdkDateTimeParam testJdkDateTimeParam=new TestJdkDateTimeParam();
        testJdkDateTimeParam.setTestDate(DateUtil.formatDate(new Date()));
        testJdkDateTimeParam.setId(SmartStringUtils.getSnowFlakeStrId());
        VueElementAdminResponse vueElementAdminResponse=testJdkDateTimeRpcService.fetchParamIncludeJdkDateTime(testJdkDateTimeParam);
        // Incorrect usage will throw an exception java.util.LinkedHashMap cannot be cast to com.xxx.business.test.model.TestJdkDateTimeParam
        // TestJdkDateTimeParam testJdkDateTimeParam1= (TestJdkDateTimeParam)vueElementAdminResponse.getData();
        // Correct usage
        //Serialize object into string
        String dataJson=SmartJackSonUtils.writeObjectToJSon(vueElementAdminResponse.getData());
        //Deserialize into object or collection
        TestJdkDateTimeParam testJdkDateTimeParam1= SmartJackSonUtils.readValueToObject(dataJson, TestJdkDateTimeParam.class);
        log.info("test:{}",testJdkDateTimeParam1);
        return vueElementAdminResponse;
    }
}