JSON-B: how to handle Lists and Maps
JSON-B brings to Java a standard Java <-> JSON mapping API. It is pretty straight forward excepted it uses Type in its API when Class<?> is not enough. What is this case? Commonly the List, Map and other structures relying on generics cause the Class<?> is in this case the wrapper but the mapper needs the generics as well to know how to map the elements of the structure.
To make it concrete let's take the simple case to deserialize a list from a string to a list of object:
final String listString = "[{\"a\":\"b\"},{\"a\":\"c\"}]";
final Jsonb jsonb = JsonbProvider.provider().create().build();
final Type type = ????;
final List<Element> elements = jsonb.fromJson(listString, type));
This example is pretty direct but question is: how to fill this Type entry to ensure the runtime knows we want a List<Element>?
If you check the hierarchy of Type, you will get:
So this is the root of all types of the JVM:
- Class: you probably know this one, it is a simple type, the class type of an object
- GenericArrayType: type of an array if including some generics, for instance: <T extends SomeType> T[] returnGenericArray();
- ParameterizedType: the most simple generic type, it is a wrapping structure (List, Map, ...) with a list of generic parameters (arguments). A simple example is a List<String>. The raw type (structure) is List and the arguments are [String].
- TypeVariable: here it starts to be more complicated - but for JSON mapping you very rarely need it or you mapping is quite complex for human beings already ;). It represents a type with bounds. It means at that moment the type is not fully resolved. Concretely take the same example than for GenericArrayType but replacing the array by a List.
- WildcardType: an unlimited type (either on upper or lower bound), for instance <? extends MyRoot> or <? super MySuperLowChild>. It is often used for lists to support polymorphism. It doesn't fit mapping out of the box and often needs an adapter to be able to wrap each element to enrich the serialization with the type. If you go this way, ensure you will not enter in the 0-day vulnerability area!
Now you are more familiar with what a Type is, you are probably super motivated to create some implementations! If so, WAIT!
JavaEE (and few other projects) already provides out of the box some ways to get them so let's reuse what you have in your stack instead of adding some code and increasing your debt!
Johnzon, the simple way
Johnzon provides a JohnzonParameterizedType which is a simple implementation of ParameterizedType built from the constructor. Typically for our original example we would do:
final Type type = new JohnzonParameterizedType(List.class, Element.class);
It is thread-safe (understand you can cache the value for the application life duration) and purely built from the parameters.
CDI, JAX-RS, Guava...
Not sure what you think but personally I find johnzon way very natural - ok I wrote it ;) - but most of framework use something else. This is the case for CDI, JAX-RS, Guava - and a few others - which are all based on a TypeLiteral, TypeToken, .... approach where you subclass a base class extracting the Type from the inheritance. In practise here is what you do:
// CDI
final Type type = new TypeLiteral<List<Element>>() {}.getType());
// JAX-RS
final Type type = new GenericType<List<Element>>() {}.getType());
// Guava
final Type type = new TypeToken<List<Element>>() {}.getType());
First it looks weird to subclass a "type extractor" ({} in previous snippets), then if you check the implementation the extraction is quite complex compared to implementing mentionned Type directly, finally it is slower (so don't forget to create it once per type and not each time!). So why doing it this way?
- You write your type the "normal" way: you don't need to split your type in raw type, arguments or something more complicated, you just write it as you would declare a field or variable: List<Element>.
- It handles more advanced types than ParameterizedType. ParameterizedType is very easy to implement but TypeVariable and WildcardType can be quite complex to build manually compared to this solution.
So what to use?
Of course the answer to such a question is always: "it depends". If you already have a stack with a type literal, I would recommand you to reuse it if you already depend on it explicitly. If you are in a EE container, CDI one would be more recommanded than JAX-RS one since CDI is far more central.
If you use a lot this feature, probably try to extract your types in a "cache" or "manager" API creating the types once for the whole application. Tip there can be to use an enum which provides a natural way to access types:
public enum Types {
LIST_ELEMENT(new TypeLiteral<List<Element>>(){}),
MAP_STRING_ELEMENT(new TypeLiteral<Map<String, Element>>(){});
private final Type type; // + getter
Types(TypeLiteral type) {
this.type = type.getType();
}
}
And finally do:
final String listString = "[{\"a\":\"b\"},{\"a\":\"c\"}]";
final Jsonb jsonb = JsonbProvider.provider().create().build();
final List<Element> elements = jsonb.fromJson(
listString,
Types.LIST_ELEMENT.getType()));
From the same author:
In the same category: