I have to configure Jackson to return parametric t...
# announcements
s
I have to configure Jackson to return parametric types and it breaks my brain. So given a parametric type that’t either a single instance or a list of that instance:
Copy code
sealed class Data<T: Any> {
    class Single<T: Any>@JsonCreator constructor(@JsonValue val single: T?): Data<T>()
    class ListOf<T: Any>@JsonCreator constructor(@JsonValue val list: List<T>): Data<T>()
}
how do I created a correct typeRef for these to methods:
Copy code
private fun <T: Any> readResponseWithSingleData(
    jsonMapper: ObjectMapper,
    json: String
): Response<T, Data.Single<T>> {
    val typeRef = object : TypeReference<Response<T, Data.Single<T>>>() {}
    return jsonMapper.readValue<Response<T, Data.Single<T>>>(json, typeRef)
}

private fun <T : Any> readResponseWithListOfData(
    jsonMapper: ObjectMapper,
    json: String
): Response<T, Data.ListOf<T>> {
    val typeRef = object : TypeReference<Response<T, Data.ListOf<T>>>() {}
    return jsonMapper.readValue<Response<T, Data.ListOf<T>>>(json, typeRef)
}
The current code will compile but because of type erasure the real type T will be discarded and a
LinkedHashMap
is always used instead (which causes a ClassCast at runtime.) I can’t make these methods inline and use reified types because I pass both methods as parameter. I’m fairly certains that the answer is to use Jackson.TypeFactory.constructParametricType (https://fasterxml.github.io/jackson-databind/javadoc/2.9/com/fasterxml/jackson/databind/type/TypeFactory.html#constructParametricType-java.lang.Class-java.lang.Class...- ) It’s just that the generic parameters T is so deep enbedded (it’s a bit more complex that the example provided in the API with
List<Set<T>>
). What would the answer be assuming that I’d pass a third argument
clazz: Class<T>
to both methods?
Response
looks like this:
Copy code
data class Response<T: Any, D: Data<T>> @JsonCreator constructor(
    @JsonProperty("data") val data: Map<String, D>?,
    @JsonProperty("errors") val errors: List<String>?
)
t
Json deserializers like Jackson (and Gson as well) cannot easily deal with generics due to type erasure. You have to pass a specific
TypeReference
which will specify the type of the generic parameter. Have a look at this answer : https://stackoverflow.com/a/11681540/8691695
s
yes, I understand the problem but the details of the solution are not obvious. I have to not only get the type for Data<T> which is probably this (for
readResponseWithListOfData
)
Copy code
val dataTypeRef = jsonMapper.getTypeFactory().constructParametricType(Data.ListOf::class.java, clazz)
but also type of Response which has two type parameters
Copy code
val responseTypeRef = ...
    return jsonMapper.readValue(json, responseTypeRef);
So the composition of the final type is my problem.
So the more complicated case is definitely the of that returns a list, so if someone could help me out by completing this piece of code, that would be appreciated:
Copy code
private fun <T : Any> readResponseWithListOfData(
    jsonMapper: ObjectMapper,
    json: String,
    clazz: Class<T>
): Response<T, Data.ListOf<T>> {
    val typeRef = ...
    return jsonMapper.readValue<Response<T, Data.ListOf<T>>>(json, typeRef)
}
so basically I’m here now:
Copy code
val tType = jsonMapper.typeFactory.constructType(classOfT)
    val tListType = jsonMapper.typeFactory.constructCollectionLikeType(List::class.java, classOfT)
    val dataType = jsonMapper.typeFactory.constructParametricType(Data.ListOf::class.java, tListType)
    val typeRef = jsonMapper.typeFactory.constructParametricType(Response::class.java, tType, dataType)
    return jsonMapper.readValue<Response<T, Data.ListOf<T>>>(json, typeRef)
which leads to
Copy code
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.util.ArrayList` out of START_OBJECT token
 at [Source: (String)"{"data":{"offers":[{"offerId":"27","offerDetail":{"created":"2019-11-18T15:28:58.251","affiliateProgram":"PRICE_RUNNER"},"affiliateRetailer":{"affiliateProgram":"PRICE_RUNNER","name":"<http://Amazon.co.uk|Amazon.co.uk>","affiliateRetailerCode":"2137","created":"2019-11-18T10:27:00.593","lastModified":"2019-11-18T10:27:00.593"},"currencyCode":"GBP","stockStatus":"IN_STOCK","price":21.99,"commissionPercentage":0.0,"offerType":"AUTO","priceWithShippingMin":21.99,"priceWithShippingMax":21.99,"leadTime":"1.00 Day","affili"[truncated 861 chars]; line: 1, column: 20] (through reference chain: uk.co.digital.engine.Response["data"]->java.util.LinkedHashMap["offers"]->java.util.ArrayList[0])
j
its possible to get lucky and create an empty array that passes the test without actually knowing the paramterized type, buying you time to move forward to the place where you have generics replacing empty array -- but you still will not genuinely be able to get the type info from the declaration. for something like
Copy code
create( Pair[] foo){
(ParameterizedType)pt=foo.getClass;
var magicTypeInfo=pt.getActualTypeArguments(){0]) //<---- do array factories here... 
}
i don't have the exact refernce to the last time i faked out gson with this, and it's a tiny bit more lines of code than what i started above.but it can get you past a contrsuctor sometimes.
s
I don’t understand that code at all. Is that Java11?!? I’ve not encountered Array Factories, probably because I use Jackson and not Gson.
j
I did use var instead of Class<T> but that's unimportant. the easy to miss part of the equation is getting (ParameterizedType)pt only happens by you casting Type to ParameterizedType, so the you can query the parameter and get enough info create an empty "thing", which will comply with parameter "thing" that is impossible for you to access. i've seen no other way
s
ParameterizedType
is not in my classpath, I guess it’s a Gson-class? I don’t use Gson (but Jackson).
I solved the issue for my by using a (pretty verbose and not very elegant, but pretty straight forward) approach, that I found in the StackOverflow thread linked by @tseisel Instead of configuring the generic javaType for Jackason, I implement a class per generic type and thereby remove the generic part. So instead of trying to configure the generic type like this:
Copy code
val typeRef = ...
mapper.readValue<Data<T>>(json, typeRef)
I create a Wrapper for each actual type like this:
Copy code
class Wrapper:Data<ActualType>()
mapper.readValue<Wrapper>(jsonString, Wrapper.class);
In endeffect I had to create 3 wrapper classes. It’s not ideal because those are basically boilerplate code, but at least they are straight forward to understand.
j
https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/ParameterizedType.html should be commonly available. The SO advice is not bad. i don't know the extent of destructuring from Jackson or gson, but if your Data types are desructurable, maybe you can get lucky with simple json list readers.