Jackson's TypeReference
allows reifying the generic type parameters of a type via subclassing. Example:
final var typeRef = new TypeReference<List<MyType<MyArg>>>() {};
final List<MyType<MyArg>> list = mapper.readValue(input, typeRef);
final MyType<MyArg> first = list.get(0);
This only works if the full type is known at time of sub classing. Extracting the conversion call in a generic method will not work:
<T> T convert(final String input) {
final var typeRef = new TypeReference<List<MyType<T>>>() {};
final List<MyType<T>> list = mapper.readValue(input, typeRef);
return list;
}
final MyType<MyArg> first = list.get(0);
(because it erases to new TypeReference<List<MyType<Object>>>() {}
, which will likely be deserialized to List<MyType<Map<String, Object>>>
).
I want to deserialize and unwrap instances of generic types, without providing the full type signature at the call site. The call site should only be concerned about the inner (wrapped) type, since that's the only type it will interact with. Given the following record definitions:
private record MyResponse<T>(MyResult<T> result) {}
private record MyResult<T>(List<T> values) {}
private record MyStringValue(String name) {
@Override public String toString() {
return "'" + name + "'";
}
}
and a method
<T> MyResult<T> convert(final String input, final TypeReference<MyResponse<T>> typeRef) {
try {
return objectMapper.readValue(input, typeRef).result();
} catch (final JsonProcessingException ex) {
throw new RuntimeException(ex);
}
}
how can I unwrap the result of this function and only return an instance of T
, without providing the full type ref TypeReference<MyResponse<MyResult<T>>>
?
I have:
<T> List<T> unwrap(final String input, final TypeReference<MyResponse<T>> typeRef) {
return convert(content, typeRef).values();
}
which must be called as:
final List<MyStringValue> values = unwrap(input, new TypeReference<MyResponse<MyResult<MyStringValue>>>() {});
// or with diamond operator:
final List<AnotherType> values = unwrap(input, new TypeReference<>() {});
Callers of unwrap
should not need to know about MyResponse
nor MyResult
. Is it possible to define unwrap
in such a way that these implementation details are hidden?
final List<MyStringValue> values = unwrap(input, new TypeReference<MyStringValue>() {});
<T> List<T> unwrap(final String input, final TypeReference<T> typeRef) { // <- callers do not need to know about MyResponse/MyResult
final TypeReference<MyResponse<MyResult<T>>> wrappedTypeRef = typeRef.wrap(MyResponse<MyResult<T>>.class); // obviously not valid Java syntax
return convert(content, typeRef).values();
}
or am I simply stuck with exposing the full type to all callers of this method due to type erasure?
(Both MyResponse
and <T>
will be different concrete types in the real code: MyResponse
has a parent/child class and T
has 3 implementations)
TypeReference
s are designed solely to be created by subclassing it.
You can add a new overload of convert
that takes a JavaType
, which you can dynamically create using TypeFactory
.
<T> MyResult<T> convert(final String input, final JavaType typeRef) {
try {
return objectMapper.<MyResponse<T>>readValue(input, typeRef).result();
} catch (final JsonProcessingException ex) {
throw new RuntimeException(ex);
}
}
<T> MyResult<T> convert(final String input, final TypeReference<MyResponse<T>> typeRef) {
return convert(input, TypeFactory.defaultInstance().constructType(typeRef));
}
Use constructParametricType
in unwrap
to create the type you want:
<T> List<T> unwrap(final String input, final TypeReference<T> typeRef) {
var typeFactory = TypeFactory.defaultInstance();
// this creates a MyResponse<MyResult<T>>
// though are you sure you want this type instead of MyResponse<T>?
var responseResultType = typeFactory.constructParametricType(
MyResponse.class,
typeFactory.constructParametricType(
MyResult.class,
typeFactory.constructType(typeRef)
)
);
return this.<T>convert(input, responseResultType).values();
}