Lucas
Lucas

Reputation: 3281

how to convert LinkedHashMap to Custom java object?

I'm trying to get the data from one app to another via RESTful WS and it works, but I cannot use this data since I cannot cast it... WS returns a List of objects like this:

{id=1, forename=John, surname=Bloggs, username=jbloggs, role=Graduate Developer, office=London, skills=[{technology=Java, experience=2.5}, {technology=Web, experience=2.0}, {technology=iOS, experience=0.0}, {technology=.NET, experience=0.0}]}

to get I it use Jackson's ObjectMapper:

ObjectMapper mapper = new ObjectMapper();

    List<ConsultantDto> list = new ArrayList<ConsultantDto>();


    try {

        list = mapper.readValue(con.getInputStream(), ArrayList.class);

    } catch (JsonGenerationException e) {

        e.printStackTrace();

    } catch (JsonMappingException e) {

        e.printStackTrace();

    } catch (IOException e) {

        e.printStackTrace();

    }

after that I have 3 lines of code:

System.out.println(list.get(0));
System.out.println(list.get(0).getForename());
return list;

return because this method's return value is passed to other webservice which displays correct data in a browser. Interesting thing happens with two printing lines, one prints the data from the top of this post ({id:1 ... }) but the other one throws exception:

java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.xxx.xxx.web.dto.rp.ConsultantDto

ConsultantDto and SkillDto are two legit classes which have all properties set to match the data from WS, all getters/setters are in place. As far as I'm concerned LinkedHashMap stores stuff as key:value pairs, so I just don't see where is this exception coming from. How can I fix it and why doesn't ObjectMapper just parse the value correctly (which it does when I get a single ConsultantDto rather than a List)?

Upvotes: 34

Views: 107285

Answers (3)

Innovationchef
Innovationchef

Reputation: 387

In my use case, I had 10 different APIs. All my API Responses were in the same json format but the content was different in one of the tags - /result/data

{
    "success": true,
    "result": {
        "type": "transactions",
        "data": {}
    },
    "error": [],
    "metadata": {
        "version": "v1",
        "code": 200,
        "desc": "OK",
        "trackingId": "TRACKINGID-1588097123800-1234567890",
        "generatedTimestamp": "2020-07-14T09:41:06.198Z"
    },
    "link": {
        "self": "/v1/myapp/customer/enquiry"
    }
}

In my spring boot code, I used the following generic method for all my API calls -

<T, R> ApiResponse<R> execute(URI uri, T entity, Class<R> clazz) {
    HttpEntity<T> httpEntity = new HttpEntity<>(entity);
    ApiResponse<R> body = this.restTemplate.exchange(uri, HttpMethod.POST, httpEntity,
            new ParameterizedTypeReference<ApiResponse<R>>() {
            }).getBody();
    return body;
}

It gave me the same error as you reported above. Based on this and some other SO answers, I tried the below as well and it did not work -

<T, R> ApiResponse<R> execute(URI uri, T entity, Class<R> clazz) {
    HttpEntity<T> httpEntity = new HttpEntity<>(entity); 
    ResponseEntity<String> response = this.scVaultCoreApi.exchange(uri, HttpMethod.POST, httpEntity, String.class);
    ObjectMapper mapper = new ObjectMapper();
    try {
        return mapper.readValue(response.getBody(), new TypeReference<ApiResponse<R>>() {
        });
    } catch (Exception e) {
        throw new RuntimeException();
    }
}

The other solution that worked was manually parsing the response stream using the ObjectMapper in jackson. But I couldn't do that for so many APIs I was using and the above error came in some APIs calls only. So, only for those APIs, instead of relying on the TypeReference conversion to the Class, I did not change the above methods that I defined, but I extracted the ApiResponse<T> as ApiResponse<Object> and parsed it as a LinkedHashMap only and created my particular class object manually.

ApiResult<MyResponse> res = execute(uri, payload, MyResponse.class).getResult();
Map<String, Map<String, Object>> map = (Map<String, Map<String, Object>>) res.getData();
MyResponse myResponse = MyResponse.builder()
    .accountBalance(new BigDecimal(map.get("key").get("balance").toString()))
    .clientName((String) map.get("key").get("clientName"))
    .build();
    

Best thing about this solution was that I did not have to change the base class execute method and the other API calls were working fine and only for the troubled API, I wrote the manual object creation code.

Upvotes: 0

Donald Choi
Donald Choi

Reputation: 371

import:

import com.fasterxml.jackson.databind.ObjectMapper;

object:

private ObjectMapper mapper = new ObjectMapper();

examples:

PremierDriverInfoVariationDTO premierDriverInfoDTO = 
mapper.convertValue(json, PremierDriverInfoVariationDTO.class); 
log.debug("premierDriverInfoDTO : {}", premierDriverInfoDTO);

or

Map<String, Boolean> map = mapper.convertValue(json, Map.class);
log.debug("result : {}", map);
assertFalse(map.get("eligiblePDJob"));

Upvotes: 7

Enno Shioji
Enno Shioji

Reputation: 26882

You need to do this:

List<ConsultantDto> myObjects =
    mapper.readValue(jsonInput, new TypeReference<List<ConsultantDto>>(){});

(From this SO answer)

The reason you have to use TypeReference is because of an unfortunate quirk of Java. If Java had a proper generics, I bet your syntax would have worked.

Upvotes: 50

Related Questions