2011-07-10

Gson v Jackson - Part 5

tl;dnr

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

Part 4 of this short series of articles ended with section 5.14.3 of the Gson user guide, "User Defined Exclusion Strategies". This fifth part continues with section 5.15 on "JSON Field Naming Support", and ends with section 5.17 on "Streaming", which is the end of the Gson user guide. Part 6 will include a summary of the key differences between Gson and Jackson noted so far, along with a table of contents listing for easy navigation to the various sections of this review.
Link To This ArticleQR Code Link To This Articlehttp://goo.gl/fWihj

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...


JSON Field Naming Support


Both Gson and Jackson provide a plug-in facility for users to provide custom translations for converting from a Java property name into a JSON element name, using either single-property-specific annotations or as naming strategies to be applied to all properties. The current release of Gson also includes a few ready-made naming strategies to choose from. The next release of Jackson should as well. The next release of Jackson should also include the ability to provide translations from the JSON element name to the Java property name. (This would make it possible to easily implement case-insensitive name matching, for example. To otherwise do so requires a custom deserializer using either Gson or Jackson.)

The Gson Code: See relevant section in the Gson user guide. (The guide demonstrates using a built-in naming policy. To specify a custom one, set an implementation of FieldNamingStrategy with GsonBuilder.setFieldNamingStrategy.)

The comparable Jackson Code:
public class JacksonPropertyTranslationSupportDemo
{
public static void main(String[] args) throws Exception
{
SomeObject someObject = new SomeObject("first", "second");

ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(
new UpperCamelCaseNamingStrategy());
System.out.println(mapper.writeValueAsString(someObject));
// {"custom_naming":"first","SomeOtherField":"second"}
}
}

class SomeObject
{
@JsonProperty("custom_naming")
public final String someField;
public final String someOtherField;

public SomeObject(String a, String b)
{
this.someField = a;
this.someOtherField = b;
}
}

class UpperCamelCaseNamingStrategy
extends PropertyNamingStrategy
{
@Override
public String nameForGetterMethod(MapperConfig<?> config,
AnnotatedMethod method, String defaultName)
{
return translate(defaultName);
}

@Override
public String nameForSetterMethod(MapperConfig<?> config,
AnnotatedMethod method, String defaultName)
{
return translate(defaultName);
}

@Override
public String nameForField(MapperConfig<?> config,
AnnotatedField field, String defaultName)
{
return translate(defaultName);
}

private String translate(String defaultName)
{
char[] nameChars = defaultName.toCharArray();
nameChars[0] = Character.toUpperCase(nameChars[0]);
return new String(nameChars);
}
}
Comparison Ratings:
  • COMPARABLE for ability to use an annotation to specify per-property translation to JSON element names
  • COMPARABLE for ability to specify naming strategy translations for all properties
  • +1 Gson for providing some built-in naming strategies
  • +1 Jackson for providing a facility to specify different translations for fields versus setter methods versus getter methods

Sharing State Across Custom Serializers and Deserializers


The same options are available to Jackson solutions as are available to Gson solutions for sharing state across custom serializers and deserializers. Following are examples using Jackson of the three suggested solutions listed in the Gson user manual. (The same solutions with Gson would be almost identical.) These examples all deserialize instances of the following data structure, with the desired outcome that the value of Bar.b is prefixed with the value of Foo.a.
class Foo
{
public String a;
public Bar bar;

@Override public String toString()
{ return String.format("foo.a=%s, bar.b=%s", a, bar.b); }
}

class Bar { public String b; }
Note that controlling the order in which the different custom deserializers opperate is likely important when sharing state between them. These examples make that easy, as the target data structure heirarchy is deserialized starting from the parent -- Foo deserialization starts before Bar deserialization.

Store shared state in static fields
public class JacksonSharedStateInStaticFieldsDemo
{
public static void main(String[] args) throws Exception
{
SimpleModule module = new SimpleModule(
"SharedStaticDataDemo", Version.unknownVersion());
module.addDeserializer(Foo.class, new FooDeserializer());
module.addDeserializer(Bar.class, new BarDeserializer());

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

// {"a":"A","foo":{"b":"B"}}
String json = "{\"a\":\"A\",\"foo\":{\"b\":\"B\"}}";

Foo foo = mapper.readValue(json, Foo.class);
System.out.println(foo);
// foo.a=A, bar.b=A.B
}
}

class FooDeserializer extends JsonDeserializer<Foo>
{
static Foo foo;

@Override
public Foo deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectCodec codec = jp.getCodec();
ObjectNode node = (ObjectNode) codec.readTree(jp);
Foo foo = new Foo();
FooDeserializer.foo = foo;
foo.a = node.get("a").getValueAsText();
foo.bar = codec.treeToValue(node.get("foo"), Bar.class);
return foo;
}
}

class BarDeserializer extends JsonDeserializer<Bar>
{
@Override
public Bar deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectCodec codec = jp.getCodec();
ObjectNode node = (ObjectNode) codec.readTree(jp);
Bar bar = new Bar();
bar.b = FooDeserializer.foo.a;
bar.b += "." + node.get("b").getValueAsText();
return bar;
}
}
Declare the serializer/deserializer as inner classes of a parent type, and use the instance fields of parent type to store shared state
public class JacksonSharedStateInParentClassDemo
{
public static void main(String[] args) throws Exception
{
JacksonSharedStateInParentClassDemo demo =
new JacksonSharedStateInParentClassDemo();

SimpleModule module = new SimpleModule(
"SharedStaticDataDemo", Version.unknownVersion());
module.addDeserializer(
Foo.class, demo.new FooDeserializer());
module.addDeserializer(
Bar.class, demo.new BarDeserializer());

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

// {"a":"A","foo":{"b":"B"}}
String json = "{\"a\":\"A\",\"foo\":{\"b\":\"B\"}}";

Foo foo = mapper.readValue(json, Foo.class);
System.out.println(foo);
// foo.a=A, bar.b=A.B
}

final Map<String, String> sharedData =
new HashMap<String, String>();

class FooDeserializer extends JsonDeserializer<Foo>
{
@Override
public Foo deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectCodec codec = jp.getCodec();
ObjectNode node = (ObjectNode) codec.readTree(jp);
Foo foo = new Foo();
foo.a = node.get("a").getValueAsText();
sharedData.put("foo.a", foo.a);
foo.bar = codec.treeToValue(node.get("foo"), Bar.class);
return foo;
}
}

class BarDeserializer extends JsonDeserializer<Bar>
{
@Override
public Bar deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectCodec codec = jp.getCodec();
ObjectNode node = (ObjectNode) codec.readTree(jp);
Bar bar = new Bar();
bar.b = sharedData.get("foo.a");
bar.b += "." + node.get("b").getValueAsText();
return bar;
}
}
}
Use Java ThreadLocal
public class JacksonSharedStateWithThreadLocalDemo
{
public static void main(String[] args) throws Exception
{
JacksonSharedStateWithThreadLocalDemo demo =
new JacksonSharedStateWithThreadLocalDemo();

SimpleModule module = new SimpleModule(
"SharedStaticDataDemo", Version.unknownVersion());
module.addDeserializer(
Foo.class, demo.new FooDeserializer());
module.addDeserializer(
Bar.class, demo.new BarDeserializer());

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

// {"a":"A","foo":{"b":"B"}}
String json = "{\"a\":\"A\",\"foo\":{\"b\":\"B\"}}";

Foo foo = mapper.readValue(json, Foo.class);
System.out.println(foo);
// foo.a=A, bar.b=A.B
}

final ThreadLocal<Foo> threadLocal = new ThreadLocal<Foo>();

class FooDeserializer extends JsonDeserializer<Foo>
{
@Override
public Foo deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectCodec codec = jp.getCodec();
ObjectNode node = (ObjectNode) codec.readTree(jp);
Foo foo = new Foo();
foo.a = node.get("a").getValueAsText();
threadLocal.set(foo);
foo.bar = codec.treeToValue(node.get("foo"), Bar.class);
threadLocal.remove();
return foo;
}
}

class BarDeserializer extends JsonDeserializer<Bar>
{
@Override
public Bar deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectCodec codec = jp.getCodec();
ObjectNode node = (ObjectNode) codec.readTree(jp);
Bar bar = new Bar();
Foo foo = threadLocal.get();
bar.b = foo.a;
bar.b += "." + node.get("b").getValueAsText();
return bar;
}
}
}
Comparison Rating: COMPARABLE for available options to share data between custom serializers and deserializers

Streaming


Gson provides API components to read and write character streams (not byte streams; unlike Jackson, which can operate on byte streams). Aside from the reasons outlined in the Gson Streaming documentation, folks might choose to use the streaming API for performance concerns. According to the latest results at https://github.com/eishay/jvm-serializers/wiki, Gson databinding is more than 6x slower than Gson stream processing. This performance increase comes at the cost of coding effort. The Gson manual stream processing solution is over 300 lines long, while the Gson databinding solution is just a few lines.

Following are the Jackson equivalent solutions of the Gson examples.

Mixed Reads Example
public class JacksonMixedReadsDemo
{
public static void main(String[] args) throws Exception
{
// [{"id":"1","body":"message 1"},
// {"id":"2","body":"message 2"}]
String json = "[{\"id\":\"1\",\"body\":\"message 1\"}," +
"{\"id\":\"2\",\"body\":\"message 2\"}]";

List<Message> messages = readJsonStream(
new ByteArrayInputStream(json.getBytes()));
System.out.println(messages);
// [{id=1, body=message 1}, {id=2, body=message 2}]
}

static List<Message> readJsonStream(InputStream in)
throws IOException
{
List<Message> messages = new ArrayList<Message>();

JsonParser parser = new JsonFactory().createJsonParser(in);
parser.setCodec(new ObjectMapper());
parser.nextValue();
while (JsonToken.START_OBJECT == parser.nextValue())
{
messages.add(parser.readValueAs(Message.class));
}
parser.close();
return messages;
}
}

class Message
{
public String id;
public String body;

@Override
public String toString()
{
return String.format("{id=%s, body=%s}", id, body);
}
}
Mixed Writes Example
public class JacksonMixedWritesDemo
{
public static void main(String[] args) throws Exception
{
List<Message> messages = new ArrayList<Message>();
messages.add(new Message("1", "message 1"));
messages.add(new Message("2", "message 2"));

ByteArrayOutputStream out = new ByteArrayOutputStream();
writeJsonStream(out, messages);
System.out.println(new String(out.toByteArray(), "UTF-8"));
// [{"id":"1","body":"message 1"},
// {"id":"2","body":"message 2"}]
}

static void writeJsonStream(OutputStream out,
List<Message> messages) throws IOException
{
JsonGenerator generator =
new JsonFactory().createJsonGenerator(out);
generator.setCodec(new ObjectMapper());
generator.writeStartArray();
for (Message message : messages)
{
generator.writeObject(message);
}
generator.writeEndArray();
generator.close();
}
}

class Message
{
public String id;
public String body;

Message(String id, String body)
{
this.id = id;
this.body = body;
}

@Override
public String toString()
{
return String.format("{id=%s, body=%s}", id, body);
}
}
Additional Code Notes: The Jackson alternative to Gson's JsonWriter.setIndent() method is to either use the built-in pretty printing functionality, by calling JsonGenerator.useDefaultPrettyPrinter(), or to specify use of a custom PrettyPrinter with a call to JsonGenerator.setPrettyPrinter().

Prettyprint Example

Note that this example does not demonstrate any pretty printing abilities. For some reason that's just what the Gson documentation chose to title this section.
public class JacksonPrettyPrintDemo
{
public static void main(String[] args) throws Exception
{
JsonFactory jsonFactory = new JsonFactory();

// [{"id":"1","body":"message 1"},
// {"id":"2","body":"message 2"}]
String json = "[{\"id\":\"1\",\"body\":\"message 1\"}," +
"{\"id\":\"2\",\"body\":\"message 2\"}]";
JsonParser parser = jsonFactory.createJsonParser(json);

StringWriter out = new StringWriter();
JsonGenerator generator =
jsonFactory.createJsonGenerator(out);

prettyprint(parser, generator);
parser.close();
generator.close();
System.out.println(out.toString());
// [{"id":"1","body":"message 1"},
// {"id":"2","body":"message 2"}]
}

static void prettyprint(JsonParser parser,
JsonGenerator generator) throws IOException
{
while (parser.nextToken() != null)
{
JsonToken token = parser.getCurrentToken();
switch (token)
{
case START_ARRAY:
generator.writeStartArray();
break;
case END_ARRAY:
generator.writeEndArray();
break;
case START_OBJECT:
generator.writeStartObject();
break;
case END_OBJECT:
generator.writeEndObject();
break;
case FIELD_NAME:
String name = parser.getCurrentName();
generator.writeFieldName(name);
break;
case VALUE_STRING:
String s = parser.getText();
generator.writeString(s);
break;
case VALUE_NUMBER_INT:
case VALUE_NUMBER_FLOAT:
String n = parser.getText();
generator.writeNumber(new BigDecimal(n));
break;
case VALUE_TRUE:
case VALUE_FALSE:
boolean b = parser.getBooleanValue();
generator.writeBoolean(b);
break;
case VALUE_NULL:
generator.writeNull();
}
}
}
}
Comparison Rating: COMPARABLE for ability to read and write JSON one token at a time

Continue to part 6...

References And Resources:

No comments:

Post a Comment