Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Returning Object with type arguments using generics and avoiding Type Safety warnings

Tags:

java

generics

I am writing a class that supplies some common utilities for my environment.

In this class, I have the following method that receives a JSON String, and is supposed to map the JSON to an appropriate Object:

public static <T> T mapJsonToObject(String json, Class<T> clazz) {
    ObjectMapper mapper = new ObjectMapper();
    T parsedObject = null;
    try {
        parsedObject = mapper.readValue(json, clazz);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return parsedObject;
}

The actual conversion from String to the appropriate class is done using the Jackson library, and is working properly, as well as this entire method.

Calls to this method such as:

Object o = MyUtilities.mapJsonToObject(jsonString, Object.class);
Point p = MyUtilities.mapJsonToObject(jsonString, Point.class);
PhoneBook pb = MyUtilities.mapJsonToObject(jsonString, PhoneBook.class);

are all working properly.

However, if I don't want to map the JSON to a specific Object, and I just want to convert it to a map using key/value pairs, like so:

HashMap<String, Object> myMap = MyUtilities.mapJsonToObject(result, HashMap.class);

I get a "Type safety: The expression of type HashMap needs unchecked conversion to conform to HashMap<String,Object>" warning from the compiler. This is due to the fact that I'm losing the generic arguments when using HashMap.class

While writing this question I actually stumbled upon this tutorial and thought of overloading the mapping method in the following way:

public static <T> T mapJsonToObject(String json, T obj) {
    ObjectMapper mapper = new ObjectMapper();
    T parsedObject = null;
    try {
        parsedObject = mapper.readValue(json, new TypeReference<T>() {});
    } catch (Exception e) {
            e.printStackTrace();            
    }
    return parsedObject;
}

This requires the passing of an actual Object which I prefer to avoid:

HashMap<String, Object> myMap = null;
myMap = MyUtilities.mapJsonToObject(result, myMap);

Any idea how to improve these two methods to avoid both the passing of an Object, and the type safety warning? Please note that I'm not looking for a solution specific to HashMap

like image 957
Dagan Sandler Avatar asked Apr 22 '14 12:04

Dagan Sandler


1 Answers

With generic types you will always face problems with type erasure. As a workaround you can perform unchecked type-cast like this:

@SuppressWarnings("unchecked")
public static <C, T extends C> C mapJsonToObject(String jsonString, Class<T> result) {
    // ... stuff ...
    return (C) parsedObject;
}

However this is pretty ugly considering the fact that you don't control what classes will Jackson actually use for the collection entries.

For a better result, you can use Jackson's special type system:

@SuppressWarnings("unchecked")
public static <C> C mapJsonToObject(String jsonString, JavaType type) {
    // ... stuff ...
    return (C) parsedObject;
}

// Check answer comments on TypeFactory#collectionType deprecation
List<MyBean> result = mapJsonToObject("[]", TypeFactory.collectionType(ArrayList.class, MyBean.class));

Or you can use Jackson's type references:

public static <T> T mapJsonToObject(String jsonString, TypeReference<T> type) {
    // ... stuff ...
}

List<MyBean> result = mapJsonToObject("[]", new TypeReference<List<Object>>() {});

I kind of like the first approach (via JavaType) as that will not create a anonymous class for every method invocation you write.


UPDATE Why is the first option ugly:

The first option works because there is close to zero relation between the generic parameter T and C. While T will be inferred based on the passed argument, C will be inferred based on the result assignment. The only constraint between those is that T MUST extend C. That is just a formality to prevent completely incorrect calls like String s = mapJsonToObject("", List.class).

The option is ugly, because you are not saying anything about the contents of the map. I.e. it is up to Jackson to decide what kind of classes it wants to use.

For example { "foo": "xxx", "bar": 1 } will be probably mapped to Map with these entries:

  • String "foo" => String "xxx"
  • String "bar" => Integer 1

But for { "foo": { "bar": 1 } } it will be probably something like this:

  • String "foo" => ObjectNode { "bar": 1 }

You can use the first options. But it will never be good for anything other than just Map<String, Object> and simple JSON strings without nested objects or arrays.

like image 192
Pavel Horal Avatar answered Oct 13 '22 15:10

Pavel Horal