2011-07-02

Gson v Jackson - Part 3

tl;dnr

Use Jackson, not Gson. Use this article as a reference for basic features.

Part 2 of this short series of articles ended with section 5.7 of the Gson user guide, titled "Serializing and Deserializing Collection with Objects of Arbitrary Types". This third part continues with section 5.8 on "Built-in Serializers and Deserializers", and ends with section 5.10.1 on "InstanceCreator for a Parameterized Type". Part 4 will continue with section 5.11 on "Compact Vs. Pretty Printing for JSON Output Format".
Link To This ArticleQR Code Link To This Articlehttp://goo.gl/HsIea

See part 6 of this series for a complete listing of and links to the various sections of this Gson user guide review.

For cross reference, readers will likely find it useful to also have the Gson user guide open in another browser window. (An archive of the Gson user guide as of 2011.06.26 is available at https://sites.google.com/site/programmerbruce/downloads/Gson_User_Guide_2011.06.26.zip.)

This information is based on Gson release 1.7.1 and Jackson release 1.8.2.

The Gson User Guide Walk-through Continued...


Built-in Serializers and Deserializers


Both Gson and Jackson have built-in support to serialize and deserialize java.net.URI and java.net.URL instances.
String urlJson = "\"http://code.google.com/p/google-gson/\"";
String uriJson = "\"/p/google-gson/\"";

Gson gson = new Gson();

URL url1 = gson.fromJson(urlJson, URL.class);
System.out.println(gson.toJson(url1));
// output: "http://code.google.com/p/google-gson/"

URI uri1 = gson.fromJson(uriJson, URI.class);
System.out.println(gson.toJson(uri1));
// output: "/p/google-gson/"

ObjectMapper mapper = new ObjectMapper();

URL url2 = mapper.readValue(urlJson, URL.class);
System.out.println(mapper.writeValueAsString(url2));
// output: "http://code.google.com/p/google-gson/"

URI uri2 = mapper.readValue(uriJson, URI.class);
System.out.println(mapper.writeValueAsString(uri2));
// output: "/p/google-gson/"
Jackson also has built-in support for Joda Time, java.util.TimeZone, java.net.InetAddress, and java.sql.Timestamp objects.

Comparison Rating: +1 Jackson for more built-in serializers and deserializers

Custom Serialization and Deserialization


The Gson Code: See relevant section in the Gson user guide.

The comparable Jackson Code (assuming the built-in Joda Time handling were not present):
public class Foo
{
public static void main(String[] args) throws Exception
{
DateTimeDeserializer deserializer =
new DateTimeDeserializer();
DateTimeSerializer serializer = new DateTimeSerializer();
SimpleModule module =
new SimpleModule("DateTimeModule",
new Version(1, 0, 0, null));
module.addDeserializer(DateTime.class, deserializer);
module.addSerializer(DateTime.class, serializer);

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);

DateTime dateTime = new DateTime();
System.out.println(dateTime);
// output: 2011-06-27T21:34:06.046-07:00

String dateTimeJson1 =
mapper.writeValueAsString(dateTime);
System.out.println(dateTimeJson1);
// output: "2011-06-27T21:41:57.479-07:00"

DateTime dateTimeCopy1 =
mapper.readValue(dateTimeJson1, DateTime.class);
System.out.println(dateTimeCopy1);
// output: 2011-06-27T21:41:57.479-07:00
}
}

class DateTimeDeserializer extends StdDeserializer<DateTime>
{
DateTimeDeserializer()
{
super(DateTime.class);
}

@Override
public DateTime deserialize(JsonParser jp,
DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
return new DateTime(jp.getText());
}
}

class DateTimeSerializer extends JsonSerializer<DateTime>
{
@Override
public void serialize(DateTime value, JsonGenerator jgen,
SerializerProvider provider)
throws IOException, JsonProcessingException
{
jgen.writeString(value.toString());
}
}
Comparison Rating: COMPARABLE

Custom Serialization and Deserialization - Finer points with Serializers and Deserializers


This section of the Gson user guide is not easy to work through, as it's prone with errors and false information.

Some of the example code related to this section lives in the JavaDocs for JsonSerializer and JsonDeserializer. Archived copies of which are available at https://sites.google.com/site/programmerbruce/downloads/gson-1.7.1-javadoc.jar.

This code does not compile. Even if mostly-obvious changes were made so that it would compile, it still doesn't work as presented. (Please, someone make sense of this mess and prove me wrong.)

The documentation also incorrectly describes current default Gson behavior. For example, the current default serialization of Id(java.lang.String, 42) is {"clazz":{},"value":42} not {"clazz":"java.lang.String","value":42}, though the claimed result certainly appears to be more desirable. (In a resent post, a Gson project manager explained that the disabled Class serialization is intentional, for security reasons, though the explanation goes on to describe that the security risk is present during deserialization. Since serialization is not the same as deserialization, I don't understand how this is an explanation for the disabled serialization.)

Some of the apparent intention of this section of the user guide is covered in the "InstanceCreator for a Parameterized Type" section.

Writing an Instance Creator


The latest release of Gson no longer needs an InstanceCreator to deserialize an instance of a class without a no-argument constructor. So, the MoneyInstanceCreator described in this section of the Gson user guide is unnecessary.
public class Foo
{
public static void main(String[] args)
{
Gson gson = new Gson();

Money m1 = new Money("42", CurrencyCode.MXP);
String json1 = gson.toJson(m1);
System.out.println(json1);
// {"value":"42","currency":"MXP"}

Money m1Copy = gson.fromJson(json1, Money.class);
System.out.println(m1Copy.value); // 42
System.out.println(m1Copy.currency.name()); // MXP
}
}

class Money
{
public String value;
public CurrencyCode currency;

public Money(String v, CurrencyCode c)
{
value = v;
currency = c;
}
}

enum CurrencyCode
{
USD, MXP
}
If a no-argument constructor is not present, then Jackson requires configuration information to know how to create instances of the target Java data structure. (Similar to how Gson previously required an InstanceCreator.) The configuration information is easily provided with annotations. If the target classes are not to be modified, then Jackson provides support to mix-in the annotations as follows.
public class Foo
{
public static void main(String[] args) throws Exception
{
Gson gson = new Gson();

Money m1 = new Money("42", CurrencyCode.MXP);
String json1 = gson.toJson(m1);
System.out.println(json1);
// {"value":"42","currency":"MXP"}

Money m1Copy = gson.fromJson(json1, Money.class);
System.out.println(m1Copy.value); // 42
System.out.println(m1Copy.currency.name()); // MXP

ObjectMapper mapper = new ObjectMapper();
String json2 = mapper.writeValueAsString(m1);
System.out.println(json2);
// {"value":"42","currency":"MXP"}

// throws JsonMappingException, complaining:
// No suitable constructor found
// Money m1Copy2 = mapper.readValue(json2, Money.class);

mapper.getDeserializationConfig().addMixInAnnotations(
Money.class, MoneyCreatorMixIn.class);
Money m1Copy2 = mapper.readValue(json2, Money.class);
System.out.println(m1Copy2.value); // 42
System.out.println(m1Copy2.currency.name()); // MXP
}
}

abstract class MoneyCreatorMixIn
{
@JsonCreator
MoneyCreatorMixIn(
@JsonProperty("value") String v,
@JsonProperty("currency") CurrencyCode c)
{

}
}

class Money
{
public String value;
public CurrencyCode currency;

public Money(String v, CurrencyCode c)
{
value = v;
currency = c;
}
}

enum CurrencyCode
{
USD, MXP
}
Comparison Ratings: +1 Gson for ability to create Java instances without no-argument constructors and without explicit configuration information

InstanceCreator for a Parameterized Type


The following review of this section of the Gson user guide is much longer than initially expected, covering uses and details of Gson and Jackson beyond Gson's InstanceCreator feature. This occurred, since the example that Gson presented introduced deserialization concerns of greater depth than simple use of an InstanceCreator. For the casual reader interested only in what a Gson InstanceCreator is and what the specific Jackson counterpart is, here's what should be understood.
  • A Gson InstanceCreator is simply used to construct an instance of an object with default values. This instance is typically later used during deserialization to replace the default values with values from the JSON. This can be useful in situations where the JSON may not contain values for all of the Java fields to be populated.
  • The Jackson equivalent approach is to just directly create an instance of the target object with default values (instead of indirectly through an InstanceCreator), and then use an updating reader to replace the values initially specified with values from the JSON. This approach is demonstrated in one of the examples below. (Search for "updatingReader".)
Back to the example in the Gson user guide...

Correcting for compiler errors and missing information, here is what I understand the example Gson code was intended to be.
public class GsonInstanceCreatorForParameterizedTypeDemo
{
public static void main(String[] args)
{
Id<String> id1 = new Id<String>(String.class, 42);

Gson gson = new GsonBuilder().registerTypeAdapter(Id.class,
new IdInstanceCreator()).create();
String json1 = gson.toJson(id1);
System.out.println(json1);
// actual output: {"classOfId":{},"value":42}
// This contradicts what the Gson docs say happens.
// With custom serialization, as described in a
// previous Gson user guide section,
// intended output may be
// {"value":42}

// input: {"value":42}
String json2 = "{\"value\":42}";

Type idOfStringType=new TypeToken<Id<String>>(){}.getType();
Id<String> id1Copy = gson.fromJson(json2, idOfStringType);
System.out.println(id1Copy);
// output: classOfId=class java.lang.String, value=42

Type idOfGsonType = new TypeToken<Id<Gson>>() {}.getType();
Id<Gson> idOfGson = gson.fromJson(json2, idOfGsonType);
System.out.println(idOfGson);
// output: classOfId=class com.google.gson.Gson, value=42
}
}

class Id<T>
{
private final Class<T> classOfId;
private final long value;

public Id(Class<T> classOfId, long value)
{
this.classOfId = classOfId;
this.value = value;
}

@Override
public String toString()
{
return "classOfId=" + classOfId + ", value=" + value;
}
}

class IdInstanceCreator implements InstanceCreator<Id<?>>
{
@SuppressWarnings({ "unchecked", "rawtypes" })
public Id<?> createInstance(Type type)
{
Type[] typeParameters =
((ParameterizedType) type).getActualTypeArguments();
Type idType = typeParameters[0];
return new Id((Class<?>) idType, 0L);
}
}
The approaches to solve this same problem with Jackson are somewhat different. The following presents a few options available.

This first simple approach "cheats" in two ways: 1. it requires that the JSON element "classOfId" have the correct, non-null value, so it fixes the input JSON accordingly; and 2. it disregards parameterized type information.
public class JacksonIdInstanceCreationDemo1
{
@SuppressWarnings("unchecked")
public static void main(String[] args) throws Exception
{
SimpleModule module =
new SimpleModule("ClassDeserializerModule",
new Version(1, 0, 0, null));
module.addDeserializer(Id.class, new IdDeserializer());

ObjectMapper mapper = new ObjectMapper().withModule(module);
mapper.getSerializationConfig()
.addMixInAnnotations(Id.class, IdCreatorMixIn.class);

Id<String> idOfString = new Id<String>(String.class, 42);

// Serializing idOfString
String idOfStringJson =
mapper.writeValueAsString(idOfString);
System.out.println(idOfStringJson);
// output: {"classOfId":"java.lang.String","value":42}

// Deserializing
// {"classOfId":"java.lang.String","value":42}
// to Id, without parameterized type info
Id<?> idOfStringCopy1 =
mapper.readValue(idOfStringJson, Id.class);
System.out.println(
mapper.writeValueAsString(idOfStringCopy1));
// output: {"classOfId":"java.lang.String","value":42}

String idOfObjectMapperJson =
setClassOfId(ObjectMapper.class, idOfStringJson, mapper);

// Deserializing
// {"classOfId":"org.codehaus.jackson.map.ObjectMapper",
// "value":42}
// to Id, without parameterized type info
Id<ObjectMapper> idOfObjectMapper =
mapper.readValue(idOfObjectMapperJson, Id.class);
System.out.println(
mapper.writeValueAsString(idOfObjectMapper));
// output: {"value":42,
// "classOfId":"org.codehaus.jackson.map.ObjectMapper"}

// input: {"value":42}
String json2 = "{\"value\":42}";

// Fixing {"value":42}
// to {"classOfId":"java.lang.String","value":42}
// Deserializing to Id, without parameterized type info
Id<String> idOfStringCopy3 = mapper.readValue(
setClassOfId(String.class, json2, mapper), Id.class);
System.out.println(
mapper.writeValueAsString(idOfStringCopy3));
// output: {"classOfId":"java.lang.String","value":42}
}

static String setClassOfId(Class<?> value,
String jsonObject, ObjectMapper mapper) throws Exception
{
ObjectNode node = (ObjectNode) mapper.readTree(jsonObject);
node.put("classOfId", value.getName());
return node.toString();
}
}

class Id<T>
{
private final Class<T> classOfId;
private final long value;

public Id(Class<T> classOfId, long value)
{
this.classOfId = classOfId;
this.value = value;
}
}

// Necessary to expose private fields for serialization.
// Deserialization just uses public constructor.
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
abstract class IdCreatorMixIn<T> {}

class IdDeserializer extends JsonDeserializer<Id<?>>
{
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Id<?> deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode node = (ObjectNode) mapper.readTree(jp);
Class<?> classOfId =
mapper.readValue(node.get("classOfId"), Class.class);
long value = mapper.readValue(node.get("value"),long.class);
return new Id(classOfId, value);
}
}
Additional Code Notes: If Jackson issue 605 were implemented, then the custom deserializer would not be necessary, and this solution could be reduced by about 31 lines. Here's what that would look like.
public class JacksonIdInstanceCreationDemo1
{
@SuppressWarnings("unchecked")
public static void main(String[] args) throws Exception
{
ObjectMapper mapper = new ObjectMapper();
mapper.getSerializationConfig()
.addMixInAnnotations(Id.class, IdCreatorMixIn.class);
mapper.getDeserializationConfig()
.addMixInAnnotations(Id.class, IdCreatorMixIn.class);

Id<String> idOfString = new Id<String>(String.class, 42);

// Serializing idOfString
String idOfStringJson =
mapper.writeValueAsString(idOfString);
System.out.println(idOfStringJson);
// output: {"classOfId":"java.lang.String","value":42}

// Deserializing
// {"classOfId":"java.lang.String","value":42}
// to Id, without parameterized type info
Id<?> idOfStringCopy1 =
mapper.readValue(idOfStringJson, Id.class);
System.out.println(
mapper.writeValueAsString(idOfStringCopy1));
// output: {"classOfId":"java.lang.String","value":42}

String idOfObjectMapperJson =
setClassOfId(ObjectMapper.class, idOfStringJson, mapper);

// Deserializing
// {"classOfId":"org.codehaus.jackson.map.ObjectMapper",
// "value":42}
// to Id, without parameterized type info
Id<ObjectMapper> idOfObjectMapper =
mapper.readValue(idOfObjectMapperJson, Id.class);
System.out.println(
mapper.writeValueAsString(idOfObjectMapper));
// output: {"value":42,
// "classOfId":"org.codehaus.jackson.map.ObjectMapper"}

// input: {"value":42}
String json2 = "{\"value\":42}";

// Fixing {"value":42}
// to {"classOfId":"java.lang.String","value":42}
// Deserializing to Id, without parameterized type info
Id<String> idOfStringCopy3 = mapper.readValue(
setClassOfId(String.class, json2, mapper), Id.class);
System.out.println(
mapper.writeValueAsString(idOfStringCopy3));
// output: {"classOfId":"java.lang.String","value":42}
}

static String setClassOfId(Class<?> value,
String jsonObject, ObjectMapper mapper) throws Exception
{
ObjectNode node = (ObjectNode) mapper.readTree(jsonObject);
node.put("classOfId", value.getName());
return node.toString();
}
}

class Id<T>
{
private final Class<T> classOfId;
private final long value;

public Id(Class<T> classOfId, long value)
{
this.classOfId = classOfId;
this.value = value;
}
}

// Necessary to expose private fields for serialization.
// Deserialization just uses public constructor.
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
abstract class IdCreatorMixIn<T>
{
@JsonCreator
public IdCreatorMixIn(@JsonProperty("idOfClass") Class<T> c,
@JsonProperty("value") long v) {}
}
This next approach more closely resembles the functionality in the original Gson example, as it uses the parameterized Id type information to determine what Class to assign to "classOfId", disregarding whatever value might be in the input JSON for this field, and allowing the JSON element "classOfId" to be missing or have a null value.
public class JacksonContextualDeserializerForRootDemo
{
public static void main(String[] args) throws Exception
{
SimpleModule module =
new CustomModule("IdDeserializerModule",
new Version(1, 0, 0, null), new IdDeserializers());
module.addDeserializer(Id.class, new IdDeserializer(null));

ObjectMapper mapper = new ObjectMapper().withModule(module);

// Necessary for serialization.
// Deserialization uses public constructor.
mapper.setVisibilityChecker(mapper.getVisibilityChecker()
.withFieldVisibility(Visibility.ANY));

Id<String> idOfString = new Id<String>(String.class, 42);

// Deserializing
// {"classOfId":"java.lang.String","value":42} to Id,
// without parameterized type information
String idOfStringJson=mapper.writeValueAsString(idOfString);
Id<?> idOfStringCopy1 =
mapper.readValue(idOfStringJson, Id.class);
System.out.println(
mapper.writeValueAsString(idOfStringCopy1));
// OUTPUT: {"classOfId":null,"value":42}

// Deserializing
// {"classOfId":"java.lang.String","value":42}
// to Id<String>
TypeReference<Id<String>> idOfStringType =
new TypeReference<Id<String>>() {};
Id<String> idOfStringCopy2 =
mapper.readValue(idOfStringJson, idOfStringType);
System.out.println(
mapper.writeValueAsString(idOfStringCopy2));
// OUTPUT: {"classOfId":"java.lang.String","value":42}

// Deserializing {"value":42} to Id<String>
String json2 = "{\"value\":42}";
Id<String> idOfStringCopy3 =
mapper.readValue(json2, idOfStringType);
System.out.println(
mapper.writeValueAsString(idOfStringCopy3));
// OUTPUT: {"classOfId":"java.lang.String","value":42}

// Deserializing {"value":42} to Id<Long>
TypeReference<Id<Long>> idOfLongType =
new TypeReference<Id<Long>>() {};
Id<Long> idOfLong = mapper.readValue(json2, idOfLongType);
System.out.println(mapper.writeValueAsString(idOfLong));
// OUTPUT: {"classOfId":"java.lang.Long","value":42}

// Deserializing
// {"classOfId":"not a null value","value":42}
// to Id<String>
String json3 =
"{\"classOfId\":\"not a null value\",\"value\":42}";
Id<String> idOfStringCopy4 =
mapper.readValue(json3, idOfStringType);
System.out.println(
mapper.writeValueAsString(idOfStringCopy4));
// OUTPUT: {"classOfId":"java.lang.String","value":42}

// Deserializing
// {"classOfId":"not a null value","value":42}
// to Id<Long>
Id<Long> idOfLong2 = mapper.readValue(json3, idOfLongType);
System.out.println(mapper.writeValueAsString(idOfLong2));
// OUTPUT: {"classOfId":"java.lang.Long","value":42}
}
}

class Id<T>
{
private final Class<T> classOfId;
private final long value;

public Id(Class<T> classOfId, long value)
{
this.classOfId = classOfId;
this.value = value;
}
}

class IdDeserializer extends JsonDeserializer<Id<?>>
implements ContextualDeserializer<Id<?>>
{
private Class<?> targetClass;

IdDeserializer(Class<?> c) {targetClass = c;}

@Override
public JsonDeserializer<Id<?>> createContextual(
DeserializationConfig config, BeanProperty property)
throws JsonMappingException
{
if (property != null)
{
JavaType type = property.getType();
JavaType ofType = type.containedType(0);
targetClass = ofType.getRawClass();
}
return this;
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Id<?> deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode object = (ObjectNode) mapper.readTree(jp);
long value = object.get("value").getLongValue();
return new Id(targetClass, value);
}
}

class IdDeserializers extends SimpleDeserializers
{
@Override
public JsonDeserializer<?> findBeanDeserializer(
JavaType type, DeserializationConfig config,
DeserializerProvider provider, BeanDescription beanDesc,
BeanProperty property) throws JsonMappingException
{
JsonDeserializer<?> deserializer =
(_classMappings == null) ? null :
_classMappings.get(new ClassKey(type.getRawClass()));
if (deserializer instanceof IdDeserializer && type != null)
{
JavaType ofType = type.containedType(0);
if (ofType != null)
{
Class<?> targetClass = ofType.getRawClass();
deserializer = new IdDeserializer(targetClass);
}
}
return deserializer;
}
}

class CustomModule extends SimpleModule
{
protected final SimpleDeserializers deserializers;

public CustomModule(String name, Version version,
SimpleDeserializers deserializers)
{
super(name, version);
this.deserializers = deserializers;
}

@Override
public <T> SimpleModule addDeserializer(
Class<T> type, JsonDeserializer<? extends T> deser)
{
deserializers.addDeserializer(type, deser);
return this;
}

@Override
public void setupModule(SetupContext context)
{
if (_serializers != null)
context.addSerializers(_serializers);
if (deserializers != null)
context.addDeserializers(deserializers);
if (_keySerializers != null)
context.addKeySerializers(_keySerializers);
if (_keyDeserializers != null)
context.addKeyDeserializers(_keyDeserializers);
if (_abstractTypes != null)
context.addAbstractTypeResolver(_abstractTypes);
}
}
Additional Code Notes:
  • That's a lot of code; 100 lines to do what Gson did in 30. Some of the additional configuration steps are to enable contextual deserialization, which is not enabled by default, in order to provide faster processing for situations that don't need it. Some parts of the additional configuration framework would not be necessary were Jackson enhanced to allow plug-in customizations of the relevant parts. From the bottom up, the following describes what it's all doing.
    • The custom SimpleModule is necessary to insert a custom SimpleDeserializers.
    • The custom SimpleDeserializers is necessary to ensure a different custom JsonDeserializer instance is used for each target type.
    • The custom JsonDeserializer is necessary to construct parameterized type-specific instances of Id.
    The end result is a solution that provides a slightly more robust solution than what the original Gson example provided. (This is explained further below.)
  • If the next release of Jackson includes an implementation for issue 599, then subclassing SimpleModule will not be necessary. This would reduce the solution by 34 lines.
  • I'll probably submit at least two more enhancement requests for simpler contextual deserialization (to remove the necessary custom Deserializers implementation), and for user-customized null or missing element deserialization.
This second Jackson approach can be made somewhat more robust, to not require hard-coding of the "classOfId" String literal, by initially constructing a partially-complete Id instance with only the classOfId set, and then using an updating ObjectReader to populate the remaining field from the JSON, without needing to access the JSON element explicitly by name. Here's what that solution would look like.
public class JacksonContextualDeserializerForRootDemo2
{
public static void main(String[] args) throws Exception
{
ObjectMapper mapper2 = new ObjectMapper();
mapper2.getDeserializationConfig()
.addMixInAnnotations(Id.class, IdCreatorMixIn.class);

SimpleModule module =
new CustomModule("IdDeserializerModule",
new Version(1, 0, 0, null), new IdDeserializers());
module.addDeserializer(
Id.class, new IdDeserializer(mapper2, null));

ObjectMapper mapper = new ObjectMapper().withModule(module);
mapper.getSerializationConfig()
.addMixInAnnotations(Id.class, IdSerializerMixIn.class);

Id<String> idOfString = new Id<String>(String.class, 42);

// Deserializing
// {"classOfId":"java.lang.String","value":42} to Id,
// without parameterized type information
String idOfStringJson=mapper.writeValueAsString(idOfString);
Id<?> idOfStringCopy1 =
mapper.readValue(idOfStringJson, Id.class);
System.out.println(
mapper.writeValueAsString(idOfStringCopy1));
// OUTPUT: {"classOfId":null,"value":42}

// Deserializing
// {"classOfId":"java.lang.String","value":42}
// to Id<String>
TypeReference<Id<String>> idOfStringType =
new TypeReference<Id<String>>() {};
Id<String> idOfStringCopy2 =
mapper.readValue(idOfStringJson, idOfStringType);
System.out.println(
mapper.writeValueAsString(idOfStringCopy2));
// OUTPUT: {"classOfId":"java.lang.String","value":42}

// Deserializing {"value":42} to Id<String>
String json2 = "{\"value\":42}";
Id<String> idOfStringCopy3 =
mapper.readValue(json2, idOfStringType);
System.out.println(
mapper.writeValueAsString(idOfStringCopy3));
// OUTPUT: {"classOfId":"java.lang.String","value":42}

// Deserializing {"value":42} to Id<Long>
TypeReference<Id<Long>> idOfLongType =
new TypeReference<Id<Long>>() {};
Id<Long> idOfLong = mapper.readValue(json2, idOfLongType);
System.out.println(mapper.writeValueAsString(idOfLong));
// OUTPUT: {"classOfId":"java.lang.Long","value":42}

// Deserializing
// {"classOfId":"not a null value","value":42}
// to Id<String>
String json3 =
"{\"classOfId\":\"not a null value\",\"value\":42}";
Id<String> idOfStringCopy4 =
mapper.readValue(json3, idOfStringType);
System.out.println(
mapper.writeValueAsString(idOfStringCopy4));
// OUTPUT: {"classOfId":"java.lang.String","value":42}

// Deserializing
// {"classOfId":"not a null value","value":42}
// to Id<Long>
Id<Long> idOfLong2 = mapper.readValue(json3, idOfLongType);
System.out.println(mapper.writeValueAsString(idOfLong2));
// OUTPUT: {"classOfId":"java.lang.Long","value":42}
}
}

class Id<T>
{
private final Class<T> classOfId;
private final long value;

public Id(Class<T> classOfId, long value)
{
this.classOfId = classOfId;
this.value = value;
}
}

// Necessary to expose private fields for serialization.
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
abstract class IdSerializerMixIn<T> {}

// Necessary to skip setting classOfId during deserialization.
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
abstract class IdCreatorMixIn<T>
{
@JsonIgnore private final Class<T> classOfId = null;
}

class IdDeserializer extends JsonDeserializer<Id<?>>
implements ContextualDeserializer<Id<?>>
{
private final ObjectMapper mapper;
private Class<?> targetClass;

IdDeserializer(ObjectMapper mapper, Class<?> c)
{
this.mapper = mapper;
this.targetClass = c;
}

ObjectMapper getMapper() {return mapper;}

@Override
public JsonDeserializer<Id<?>> createContextual(
DeserializationConfig config, BeanProperty property)
throws JsonMappingException
{
if (property != null)
{
JavaType type = property.getType();
JavaType ofType = type.containedType(0);
targetClass = ofType.getRawClass();
}
return this;
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Id<?> deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
Id id = new Id(targetClass, 0);
return mapper.updatingReader(id).readValue(jp);
}
}

class IdDeserializers extends SimpleDeserializers
{
@Override
public JsonDeserializer<?> findBeanDeserializer(
JavaType type, DeserializationConfig config,
DeserializerProvider provider, BeanDescription beanDesc,
BeanProperty property) throws JsonMappingException
{
JsonDeserializer<?> deserializer =
(_classMappings == null) ? null :
_classMappings.get(new ClassKey(type.getRawClass()));
if (deserializer instanceof IdDeserializer && type != null)
{
JavaType ofType = type.containedType(0);
if (ofType != null)
{
Class<?> targetClass = ofType.getRawClass();
deserializer = new IdDeserializer(
((IdDeserializer)deserializer).getMapper(), targetClass);
}
}
return deserializer;
}
}

class CustomModule extends SimpleModule
{
protected final SimpleDeserializers deserializers;

public CustomModule(String name, Version version,
SimpleDeserializers deserializers)
{
super(name, version);
this.deserializers = deserializers;
}

@Override
public <T> SimpleModule addDeserializer(
Class<T> type, JsonDeserializer<? extends T> deser)
{
deserializers.addDeserializer(type, deser);
return this;
}

@Override
public void setupModule(SetupContext context)
{
if (_serializers != null)
context.addSerializers(_serializers);
if (deserializers != null)
context.addDeserializers(deserializers);
if (_keySerializers != null)
context.addKeySerializers(_keySerializers);
if (_keyDeserializers != null)
context.addKeyDeserializers(_keyDeserializers);
if (_abstractTypes != null)
context.addAbstractTypeResolver(_abstractTypes);
}
}
In the custom deserializer, instead of using the original ObjectMapper available through the JsonParser, use of the second ObjectMapper instance is necessary, because the original ObjectMapper is configured to just pass deserialization back to the custom deserializer (by calling a different method that we've chosen not to implement). It's possible to handle this call back, but then manual parsing would be necessary, including explicit reference to the JSON element "classOfId" by name, which would defeat the purpose of this second approach (which is to not need hard-coded reference of "classOfId").

Going back to the first Gson demo in this section of the Gson user guide -- we're still in the "InstanceCreator for a Parameterized Type" section -- as written, it actually fails, throwing an exception, if the JSON contains a non-null value for the "classOfId" element. Also, if the JSON contains a null value for the "classOfId" element, then the Gson solution replaces the Id.classOfId value set by the InstanceCreator with the null value from the JSON. This happens because Gson first uses the instance creator to create an instance of the target type with any state (i.e., properties values) specified by the instance creator, and then Gson replaces that state, as appropriate, with values from the JSON. As mentioned, this means that the "classOfId" field is overwritten if the JSON has an element of the same name (unless something is explicitly done to stop this from happening). To further clarify this point, following is a demo of these shortcomings.
  // input: {"classOfId":"java.lang.Long","value":42}
String json3 =
"{\"classOfId\":\"java.lang.Long\",\"value\":42}";
// Id<String> id1Copy2 = gson.fromJson(json3, idOfStringType);
// throws RuntimeException:
// Unable to invoke no-args constructor for
// java.lang.Class<java.lang.String>

// input: {"classOfId":null,"value":42}
String json4 = "{\"classOfId\":null,\"value\":42}";
Id<String> id1Copy3 = gson.fromJson(json4, idOfStringType);
System.out.println(id1Copy3);
// output: classOfId=null, value=42
A decent solution to solve these shortcomings would be to implement a custom deserializer for Id, instead of a custom instance creator. The following code demonstrates this alternative, more robust implementation.
public class GsonInstanceCreatorForParameterizedTypeDemo2
{
public static void main(String[] args)
{
Id<String> id1 = new Id<String>(String.class, 42);

Gson gson = new GsonBuilder().registerTypeAdapter(Id.class,
new IdDeserializer()).create();
String json1 = gson.toJson(id1);
System.out.println(json1);
// actual output: {"classOfId":{},"value":42}
// This contradicts what the Gson docs say happens.
// With custom serialization, as described in a
// previous Gson user guide section,
// intended output may be
// {"value":42}

// input: {"value":42}
String json2 = "{\"value\":42}";

Type idOfStringType=new TypeToken<Id<String>>(){}.getType();
Id<String> id1Copy = gson.fromJson(json2, idOfStringType);
System.out.println(id1Copy);
// output: classOfId=class java.lang.String, value=42

Type idOfGsonType = new TypeToken<Id<Gson>>() {}.getType();
Id<Gson> idOfGson = gson.fromJson(json2, idOfGsonType);
System.out.println(idOfGson);
// output: classOfId=class com.google.gson.Gson, value=42

// input: {"classOfId":"java.lang.Long","value":42}
String json3 =
"{\"classOfId\":\"java.lang.Long\",\"value\":42}";
Id<String> id1Copy2 = gson.fromJson(json3, idOfStringType);
System.out.println(id1Copy2);
// output: classOfId=class java.lang.String, value=42

// input: {"classOfId":null,"value":42}
String json4 = "{\"classOfId\":null,\"value\":42}";
Id<String> id1Copy3 = gson.fromJson(json4, idOfStringType);
System.out.println(id1Copy3);
// output: classOfId=class java.lang.String, value=42

// input: {"classOfId":"a non-null value","value":42}
String json5 =
"{\"classOfId\":\"a non-null value\",\"value\":42}";
Id<Gson> idOfGson2 = gson.fromJson(json5, idOfGsonType);
System.out.println(idOfGson2);
// output: classOfId=class com.google.gson.Gson, value=42
}
}

class Id<T>
{
private final Class<T> classOfId;
private final long value;

public Id(Class<T> classOfId, long value)
{
this.classOfId = classOfId;
this.value = value;
}

@Override
public String toString()
{
return "classOfId=" + classOfId + ", value=" + value;
}
}

class IdDeserializer implements JsonDeserializer<Id<?>>
{
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Id<?> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context)
throws JsonParseException
{
long value =json.getAsJsonObject().get("value").getAsLong();
Type[] typeParameters =
((ParameterizedType) typeOfT).getActualTypeArguments();
Type idType = typeParameters[0];
return new Id((Class<?>) idType, value);
}
}
If the Id class definition could be modified, which violates a condition specified by the Gson user guide for this section that the Id class was part of an API that could not be modified, then the @Expose annotation (covered in more detail in part 4 of this series) could be employed to similarly skip replacing the value of classOfId, initially set by the instance creator, with whatever is in the JSON. (If Gson had a facility to mix in annotations like Jackson does, then modifying the Id class definition would not be necessary for this approach.) Here's what that solution would look like.
public class GsonInstanceCreatorForParameterizedTypeDemo3
{
public static void main(String[] args)
{
Id<String> id1 = new Id<String>(String.class, 42);

Gson gson = new GsonBuilder()
.registerTypeAdapter(Id.class, new IdInstanceCreator())
.excludeFieldsWithoutExposeAnnotation().create();
String json1 = gson.toJson(id1);
System.out.println(json1);
// actual output: {"classOfId":{},"value":42}
// This contradicts what the Gson docs say happens.
// With custom serialization, as described in a
// previous Gson user guide section,
// intended output may be
// {"value":42}

// input: {"value":42}
String json2 = "{\"value\":42}";

Type idOfStringType=new TypeToken<Id<String>>(){}.getType();
Id<String> id1Copy = gson.fromJson(json2, idOfStringType);
System.out.println(id1Copy);
// output: classOfId=class java.lang.String, value=42

Type idOfGsonType = new TypeToken<Id<Gson>>() {}.getType();
Id<Gson> idOfGson = gson.fromJson(json2, idOfGsonType);
System.out.println(idOfGson);
// output: classOfId=class com.google.gson.Gson, value=42

// input: {"classOfId":"java.lang.Long","value":42}
String json3 =
"{\"classOfId\":\"java.lang.Long\",\"value\":42}";
Id<String> id1Copy2 = gson.fromJson(json3, idOfStringType);
System.out.println(id1Copy2);
// output: classOfId=class java.lang.String, value=42

// input: {"classOfId":null,"value":42}
String json4 = "{\"classOfId\":null,\"value\":42}";
Id<String> id1Copy3 = gson.fromJson(json4, idOfStringType);
System.out.println(id1Copy3);
// output: classOfId=class java.lang.String, value=42

// input: {"classOfId":"a non-null value","value":42}
String json5 =
"{\"classOfId\":\"a non-null value\",\"value\":42}";
Id<Gson> idOfGson2 = gson.fromJson(json5, idOfGsonType);
System.out.println(idOfGson2);
// output: classOfId=class com.google.gson.Gson, value=42
}
}

class Id<T>
{
private final Class<T> classOfId;
@Expose private final long value;

public Id(Class<T> classOfId, long value)
{
this.classOfId = classOfId;
this.value = value;
}

@Override
public String toString()
{
return "classOfId=" + classOfId + ", value=" + value;
}
}

class IdInstanceCreator implements InstanceCreator<Id<?>>
{
@SuppressWarnings({ "unchecked", "rawtypes" })
public Id<?> createInstance(Type type)
{
Type[] typeParameters =
((ParameterizedType) type).getActualTypeArguments();
Type idType = typeParameters[0];
return new Id((Class<?>) idType, 0L);
}
}
Comparison Ratings:
  • COMPARABLE in ability to deserialize with parameterized types
  • +1 Gson for simple, contextual deserialization with parameterized type information
  • COMPARABLE in ability to accommodate missing JSON element values -- Gson uses an InstanceCreator to set the default value and further deserialization processing to overwrite value from JSON as appropriate; Jackson uses an updating reader for the same effect

Continue to part 4...

References And Resources:

3 comments:

  1. Very thorough and deep-cutting comparison, great!

    One very minor addition: there is a Jira enhancement request for Jackson to be able to instantiate types without default constructor has been requests, see [http://jira.codehaus.org/browse/JACKSON-286]. I don't know if this is approach GSON uses, but I know XStream uses that trick when running on Sun JVM.

    ReplyDelete
  2. Yes, since the 1.7 release, Gson uses sun.misc.Unsafe to initially allocate object instances.

    ReplyDelete
  3. I find Gson being much easier to use for custom serializers. E.g. see http://stackoverflow.com/questions/7161638/how-do-i-use-a-custom-serializer-with-jackson And the documentation for Gson is much better than the documentation for Jackson. E.g. you link to the Gson-documentation for examples, but there are few good examples for Jackson.

    ReplyDelete