I am trying to serialize instances of a generic class while preserving the generic types, so I will be able to deserialize it later without having to specify the generic type manually.
I understand that the usual way of deserializing generic objects is by using type references or JavaType objects like this:
ObjectMapper om = new ObjectMapper();
ObjectReader or = om.reader();
ObjectWriter ow = om.writer();
String json = "[1, 2, 3]"
JavaType listType = or.getTypeFactory()
.constructParametricType(List.class, Integer.class);
List<Integer> integers = or.forType(listType).readValue(json)
But I do not know the generic type (Integer in this case) beforehand, therefore I can't do this.
I also understand that, because of erasure, I'll have to include the type information within the serialized JSON in some way or another. This can be done with the #JsonTypeInfo annotation:
class Pojo<T> {
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public T value;
}
However, this quickly becomes bloated if the type T is used in various other places. Consider the following example:
class Pojo<T> {
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public T value;
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public List<T> otherStuff;
// constructor
}
// ...
Pojo<BigDecimal> pojo = new Pojo<>(
BigDecimal.valueOf(42),
Lists.newArrayList(BigDecimal.valueOf(14), BigDecimal.valueOf(23))
);
final String json = ow.writeValueAsString(pojo);
System.out.println(json);
produces the following result:
{
"value": ["java.math.BigDecimal", 42],
"otherStuff":[
["java.math.BigDecimal", 14],
["java.math.BigDecimal", 23]
]
}
which repeats the type, BigDecimal in this case, for every single object. This is unnecessary because the type is the same for all occurances of T anyway (except in some polymorphic cases I suppose).
If the #JsonTypeInfo annotation is omitted for otherStuff, Jackson is unable to defer the type of the contents of otherStuff from the type of value. In this example it will deserialize otherStuff as List<Integer>, even though value is of type BigDecimal.
How can I serialize instances of a generic class, so that I can safely deserialize them later and retain the generic argument?
The type information indeed needs to be included in the serialized json string, but only once. The easiest way I know of is to write a custom creator method using the #JsonCreator annotation to perform the deserialization in two steps:
Let Jackson deserialize all non-generic fields, and one generic field that includes the type information. Capture the other generic fields as raw JsonNode, so they can be manually deserialized.
Take that type information at runtime to deserialize the remaining fields.
For the example above this would look like this (exception handling code omitted):
class Pojo<T> {
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public T value;
public List<T> otherStuff;
// constructor
#JsonCreator
public static <T> Pojo<T> jsonCreator(
#JsonProperty("value") T value,
#JsonProperty("otherStuff") JsonNode otherStuffRaw) {
JavaType listType = or.getTypeFactory()
.constructParametricType(List.class, value.getClass());
return new Pojo<T>(
value,
or.forType(listType).readValue(otherStuffRaw)
);
}
}
This, however, allows value to be a subclass of T, which might produce unexpected results. If this is a concern, another approach might be using Class<T> to retain the exact type of T. This would also shield against value possibly being null.
Related
I have a model that looks like this:
#JsonIgnoreProperties(ignoreUnknown = true)
public class InputMessage<T> {
#JsonProperty("UUID")
private String UUID;
#JsonProperty("MessageType")
private String messageType;
#JsonProperty("KeyData")
private T keyData;
...
getters/setters
}
This will be in a library that will be called from arbitrary clients, so the KeyData field has a generic type. If I try to make a call like the following from the client code, I get a ClassCastException java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class model.KeyData:
Edit :
Try with constructParametricType() advise but always an error.
ObjectMapper objectMapper = new ObjectMapper();
JavaType type = objectMapper.getTypeFactory().constructParametricType(InputMessage.class, KeyData.class);
KeyData keyData = objectMapper.readValue(inputMessage.getKeyData().toString(), type);
The json that I'm attempting to deserialize looks like this:
{
UUID: 9bae9a6a-5553-4716-8a85-995f36df7732,
KeyData: {
CNSM_ID: 2,
LGCY_SRC_ID: 123,
PARTN_NBR: 1,
PCD_EFF_DT: 2019-01-01,
SRC_CD: AB
},
MessageType: provider_selection,
Partition: 3,
Rows: [
{
Type: l_cov_prdt_pcd_w_srch,
SchemaID: 2,
Value: base64encoded value
}
]
}
The library I'm using to deserialize the json is com.fasterxml.jackson.core:jackson-databind:2.9.8
Any help with this would be greatly appreciated.
When you deserialize a JSON to a generic class, Jackson cannot guess the generic type used since that is not an information present in the JSON.
And when it doesn't know how to deserialize a field, it uses a java.util.LinkedHashMap as target.
What you want is a generic type such as :
InputMessage<KeyData> inputMessage = ...;
KeyData keyData = inputMessage.getKeyData();
An elegant way to solve that is defining a Jackson JavaType for the class by specifying the expected generic.
TypeFactory.constructParametricType(Class parametrized, Class... parameterClasses) allows that.
Supposing you want to deserialize to InputMessage<KeyData>, you can do :
JavaType type = mapper.getTypeFactory().constructParametricType(InputMessage.class, KeyData.class);
InputMessage<KeyData> keyData = mapper.readValue(json, type);
About your comment :
The library code with the generic type knows nothing about the
KeyData class, so I assume it belongs in the client code?
The library doesn't need to know this class but clients should however pass the class to perform correctly the deserialization and to return a generic instance to the client and not a raw type.
For example, clients could use the library in this way :
InputMessage<KeyData> inputMessage = myJsonLibrary.readValue("someValueIfNeeded", KeyData.class);
Since few days ago I started to work on a webservice project. This project is using Jackson to marshalling and unmarshalling JSON objects. So my question is:
Why always I have to put the {} when I am creating an instance of TypeReference? I know the constructor is protected, but why is protected? I think that it's like a hack to make visible the constructor creating an implementation of the constructor since TypeReference is abstract and you can do it. But what is the point of this?
String jsonString = "{\" firstName\":\"John\",\"lastName\":\"Chen\"}";
ObjectMapper objectMapper = new ObjectMapper();
// properties will store name and value pairs read from jsonString
Map<String, String> properties = objectMapper.readvalue(
jsonString, new TypeReference<Map<String, String>>()
{ //
});
TL;DR
Via subclassing it is possible for TypeReference to extract the actual generic type parameter. E.g:
TypeReference<String> ref = new TypeReference<String>(){};
System.out.println(ref.getType());
Prints:
class java.lang.String
This can be useful when you can't use normal classes. E.g when this doesn't work:
// doesn't work
Type type = ArrayList<String>.class;
You still can get that class by using a TypeReference:
// will yield Class<ArrayList<String>>>
Type type = new TypeReference<ArrayList<String>>(){}.getType();
Detailed
When looking at the source code of TypeReference (using Jackson 2.8.5) you can see that the constructor body contains the following lines:
Type superClass = getClass().getGenericSuperclass();
if (superClass instanceof Class<?>) { // sanity check, should never happen
throw new IllegalArgumentException("Internal error: TypeReference constructed without actual type information");
}
_type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
The interesting lines are the first and last. Let's take a closer look at the first line:
Type superClass = getClass().getGenericSuperclass();
For example when you're creating a subclass, by using an anonymous class:
TypeReference<SomeStype> ref = new TypeReference<SomeType>(){};
Then getClass returns the current Class object (an anonymous class), and getGenericSuperclass() will return the Class object from the class the current implementation extends from, in our case, superClass will equal Class<TypeReference<?>>.
Now when looking at the last line from the constructor body:
_type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
As we know that the superClass is the Class object for TypeReference<?> we know that it has a generic parameter. Hence the cast to ParameterizedType. This specified Type has the method getActualyTypeArguments() which returns an array of all generic parameters specified by that class. In our case it's just 1. So [0] will yield the first element. In the example we will get the actually specified type parameter SomeType.
I am running into an odd situation with GSON polymorphic objects in my current project. The situation is this: I have two different abstract base classes with two different use cases:
the first class is contained within a List in and of itself, and
the second class is also contained within a List, but the list is part of a larger parent object
Simplified versions of contrived classes (constructors and accessors left out for brevity; discriminator fields defined but commented out for illustration):
public abstract class ClassNumeric {
//private String numericType;
}
public class ClassOne extends ClassNumeric {
private String hex;
}
public class ClassTwo extends ClassNumeric {
private Integer whole;
private Integer fraction;
}
public abstract class ClassAlphabetic {
//private String alphabeticType;
}
public class ClassAlpha extends ClassAlphabetic {
private String name;
}
public class ClassBravo extends ClassAlphabetic {
private Integer age;
private Integer numberOfMarbles;
}
public class Container {
private String group;
private List<ClassAlphabetic> alphabetics;
}
Adapter factories and their registrations with GSON:
public RuntimeTypeAdapterFactory<ClassNumeric> numericTypeFactory = RuntimeTypeAdapterFactory
.of(ClassNumeric.class, "numericType")
.registerSubtype(ClassOne.class)
.registerSubtype(ClassTwo.class);
public RuntimeTypeAdapterFactory<ClassAlphabetic> alphabeticTypeFactory = RuntimeTypeAdapterFactory
.of(ClassAlphabetic.class, "alphabeticType")
.registerSubtype(ClassAlpha.class)
.registerSubtype(ClassBravo.class);
public final Gson gson = new GsonBuilder()
.setPrettyPrinting()
.disableHtmlEscaping()
.registerTypeAdapterFactory(numericTypeFactory)
.registerTypeAdapterFactory(alphabeticTypeFactory)
.create();
Based on what I've read so far, I don't have to (and actually aren't supposed to) declare the discriminator field in the base classes because GSON handles those internally as the JSON is serialized and deserialized.
Here is an example of how these can be used:
ClassOne c1 = ClassOne.builder().hex("EF8A").build();
ClassTwo c2 = ClassTwo.builder().whole(1).fraction(3).build();
List<ClassNumeric> numerics = Arrays.asList(c1, c2); // List of child objects
log.debug("Numerics: " + gson.toJson(numerics));
ClassAlpha ca = ClassAlpha.builder().name("Fred").build();
ClassBravo cb = ClassBravo.builder().age(5).numberOfMarbles(42).build();
List<ClassAlphabetic> alphas = Arrays.asList(ca, cb);
Container container = Container.builder().group("Test Group 1").alphabetics(alpha).build(); // List of objects field on larger object
log.debug("Alphas (container): " + gson.toJson(container));
The problem I'm having is that the ClassAlphabetic objects work fine (the discriminator field is present in the JSON), while the ClassNumeric objects do not (discriminator field missing). Sample output:
09:12:17.910 [main] DEBUG c.s.s.s.s.GSONPolymorphismTest - Numerics: [
{
"hex": "EF8A"
},
{
"whole": 1,
"fraction": 3
}
]
09:12:17.926 [main] DEBUG c.s.s.s.s.GSONPolymorphismTest - Alphas (container): {
"group": "Test Group 1",
"alphabetics": [
{
"alphabeticType": "ClassAlpha",
"name": "Fred"
},
{
"alphabeticType": "ClassBravo",
"age": 5,
"numberOfMarbles": 42
}
]
}
What am I missing here? These are essentially defined and set up with GSON the same way, but one use case works where the other doesn't.
This is because of how generics work in Java. In short, a particular generic class instance does not have any type parameterization information as a part of the instance. Type parameters, however, can be stored in field types, method return types, methods parameters, inherited super classes (say, extends ArrayList<Integer>), custom parameterized type information instances, etc. Local variables, like what numerics are, keep type parameters during compile time and exist in your and compiler minds -- due to type erasure. So, it's like a raw List in runtime. Similarly tonumerics, alphas does not have any runtime parameterization, but, unlike local variables, the Container.alphabetics field has type information that's preserved in runtime -- fields can provide type information in full. And this is used by Gson to determine which (de)serialization strategy to apply. Similarly, Gson uses default strategies when there is no additional type parameter information provided (like for the local variables). As I mentioned above, you can also create a custom parameterized type ParameterizedType to provide raw type and its type parameters information. How it can help? If you take a closer look at Gson toJson overloads, you can see that one of its overloads accepts an additional parameter (I've chosed the simplest one). This can be considered a sort of hint to Gson to tell it the exact type of the passed instance (not necessarily matches, but it should in very most cases). So, just to make it work, tell Gson your numerics List type parameterization:
// "Canonical" way: TypeToken analyzes its superclass parameterization and returns its via the getType method
private static final Type classNumericListType = new TypeToken<List<ClassNumeric>>() {
}.getType()));
System.out.println("Numerics: " + gson.toJson(numerics, classNumericListType);
Or, if you can use Gson 2.8.0+,
private static final Type classNumericListType = TypeToken.getParameterized(List.class, ClassNumeric.class);
System.out.println("Numerics: " + gson.toJson(numerics, classNumericListType);
Or simply create your own ParameterizedType implementation. I would go with type tokens when the type is known at compile time, and TypeToken.getParameterized if the type is only known at runtime. Having that, passing the type instance triggers the RuntimeTypeAdapterFactory mechanism (now it's ClassNumeric, and not raw Object), so the output is as follows:
Numerics: [
{
"numericType": "N1",
"hex": "EF8A"
},
{
"numericType": "N2",
"whole": 1,
"fraction": 3
}
]
I have an HTTP request handler that returns deserialized JSON an Object, which is derived from an abstract Request and then cast to the correct class. I'm having some issues making this handler return a List though.
public class ListMyResource extends AbstractRequest
{
public boolean isCollection;
public ListTransactions()
{
this.isCollection = true;
}
public final RequestMethod REQUEST_METHOD = RequestMethod.GET;
public final String URL_METHOD = "";
public Class<ArrayList<MyResource>> getResourceClass()
{
return new ArrayList<MyResource>().getClass();
}
}
public Object getDeserializedResponse()
{
Response response = new Response(this.request, true);
try
{
if (this.request.isCollection())
{
List<this.request.getResourceClass()> list = new ArrayList<this.request.getResourceClass()>();
listType = new TypeToken<ArrayList<AbstractObject>>() { }.getType();
response.setData(deserialize.fromJson(this.getResponse(), listType));
}
else
{
response.setData(deserialize.fromJson(this.getResponse(), this.request.getClazz()));
}
}
catch (JsonSyntaxException jse)
{
MyLibrary.LOG.error("Could not parse JSON", jse);
response.setRequestWasSuccessfull(false);
}
return response;
}
The above code works fine if isCollection() returns false and the method only has to come up with a single deserialized object, but it doesn't work for collections as I can't put the result from getResourceClass() in the <> from the List. That results in Identifier expected. How can I approach this so I achieve the desired result?
ArrayList<this.request.getResourceClass()>
This cannot work. Java type parameters must be compile-time "constants" (I mean, something known to the compiler). So,
List<String> strings
is a valid syntax for the type parameters, but
List<some.runtime.expression.here()>
is not. Next thing, I would strongly recommend you not to use Class<?> to pass data type information. Note that Class<?> holds information about a real type in your system. Consider you want to have a list of strings, a list of integers and a list of booleans types. You can't do ArrayList<String>.class -- it's an illegal expression in Java, because actual type parameters are not a part of class information. Can you subclass ArrayList with something like extends ArrayList<String> and so on? You can but you shouldn't. What if the class to subclass is final or you're going to use LinkedList? Some sort of code bloat, isn't it?
Class is java.lang.reflect.Type, and Gson requires an instance of the latter to be passed to the fromJson() method. What if you construct it yourself? Gson provides a convenient mechanism to construct java.lang.reflect.Type and java.lang.reflect.ParameterizedType (the latter is used for collections very intensively in Gson by the way, see more for Gson TypeTokens). The are two ways:
TypeToken.getParameterized(rawType, typeParameter1, typeParameter2)
For example, TypeToken.getParameterized(List.class, String.class).getType() will return a ParameterizedType instance as if it you could write List<String>.class. This is a truly dynamic approach, and you can pass some runtime execution results to the getParameterized() method.
TypeToken subclassing
The trick here is that TypeToken is an abstract class and it can be parameterized, letting Gson analyze a usually anonymous class for the actual parameters at runtime and let the getType() method return a compile-time composed type: new TypeToken<List<String>>(){}.getType() (subclasses can store information about their super classes parameterization, and Gson uses it at runtime). Note that getRawType returns List.class only - this is all just how Java generics are implemented since Class cannot store actual type parameters (read more for Java generics and erasure).
Having this vague explanation in mind, just refactor your getDeserializedResponse() method making a request return java.lang.reflect.Type rather than Class<?>. I even think that you can remove isCollection from that method and just let Gson do it all itself: Gson can distinguish between List<String> and List<Integer> types.
Assume serialization to json includes the class name of the actual object, using this annotation on the Class:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#type")
class MyClass {
String foo;
}
So json is for example:
{"#type": "com.example.MyClass", "foo": "bar"}
Can this be deserialized without specifying the type? And I mean not even the super type. Just something like:
objectMapper.readValue(value, Object.class);
which doesn't actually work, it brings back a Map.
Well, it is certainly possible to do that although I have personally never used Jackson that way. You can deserialize it to a JsonNode object and then convert it to the proper type.
final ObjectMapper objectMapper = new ObjectMapper();
final MyClass myClass = new MyClass();
myClass.foo = "bar";
// Serialize
final String json = objectMapper.writeValueAsString(myClass);
// Deserialize
final JsonNode jsonNode = objectMapper.readTree(json);
// Get the #type
final String type = jsonNode.get("#type").asText();
// Create a Class-object
final Class<?> cls = Class.forName(type);
// And convert it
final Object o = objectMapper.convertValue(jsonNode, cls);
System.out.println(o.getClass());
The output is:
MyClass
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
MyClass original = new MyClass();
original.foo = "hello";
String json = mapper.writeValueAsString(original);
MyClass foo = (MyClass) mapper.readValue(json, MyClass.class);
This should work and is very convenient.
Yes, but there is a caveat: type you give MUST be something that includes #JsonTypeInfo you specify. Object.class will not have, unless you use "mix-in annotations" to associate it.
However, if you need to add type information for properties of (declared type of) java.lang.Object, you probably want to enable default typing: see ObjectMapper.enableDefaultTyping(...) for more information.
That will actually enable inclusion (and use) of type information for larger categories of classes, without need to add annotations.
What you want to use is Jackson Polymorphic Deserialization.