Jackson is a mainstay of Java web development, providing JSON serialization & deserialization to Jersey, RestEasy, and Hadoop, among many more. Jackson can be easily integrated into most applications through the use of its data-binding annotations—simply mark fields to serialize with @JsonProperty
and Jackson will happily create & read JSON representations of your object when you hand them to an ObjectMapper. Sometimes, though, you don’t have access to or don’t want to modify the source of an object that you want to use with Jackson. In these cases, you can write a custom module to provide Jackson with the information it needs to (de)JSONify your objects.
We recently ran across the no known serializer problem while trying to use Jackson (by way of Jersey) with an object that contained a Joda Time Duration. For future reference, the exception produced by Jersey looks like this:
com.sun.jersey.spi.container.ContainerResponse - The exception contained within MappableContainerException could not be mapped to a response, re-throwing to the HTTP container com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class org.joda.time.Duration]: can not instantiate from JSON object (need to add/enable type information?) at [Source: org.eclipse.jetty.server.HttpInput@1c7b0f4d; line: 1, column: 47] at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:163) ~[jackson-databind-2.0.1.jar:na]
Since our application employed the Joda data type extensions module for Jackson, this error seemed curious, but looking into that project it became clear that translation is not provided for all of the Joda data types. Duration is an exceedingly simple object (it can be represented as a number of milliseconds), so writing our own serializer & deserializer is an easy endeavor.
public final class DurationSerializer extends StdScalarSerializer<Duration> { public DurationSerializer() { super(Duration.class); } @Override public void serialize(Duration value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { jgen.writeNumber(value.getMillis()); } }
Exceedingly simple, no? As mentioned before, a Duration is nothing more than an elapsed amount of time which we represent as milliseconds for ease of JSONing. Writing the deserializer to pull a Duration out of JSON is a similarly trivial bit of code:
public final class DurationDeserializer extends StdScalarDeserializer<Duration> { public DurationDeserializer() { super(Duration.class); } @Override public Duration deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { if (jsonParser.getCurrentToken() == VALUE_NUMBER_INT) { return new Duration(jsonParser.getLongValue()); } throw deserializationContext.mappingException("Expected JSON Number"); } }
Easy, we’re just verifying that the JSON token is an integer and then passing it to the Duration constructor. These two classes are the actual functionality of JSON reading and writing, but we need to tell Jackson about them. To do so, we’ll create a Jackson data-binding module:
public final class JodaDurationModule extends SimpleModule { public JodaDurationModule() { addDeserializer(Duration.class, new DurationDeserializer()); addSerializer(Duration.class, new DurationSerializer()); } }
Now, tell Jackson about the module:
ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerModule(new JodaDurationModule());
With that, Jackson now knows how to translate Joda Durations into JSON and back:
System.out.println(objectMapper.writer().writeValueAsString(new Duration(8)));
For completeness, here are the Maven dependency clauses for Jackson. The above code only requires jackson-core
, but I mention the data-binding annotations and the Joda Time extensions:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.0.1</version> </dependency> <!-- For the aforementioned data-binding annotations --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.0.1</version> </dependency> <!-- Extensions for Joda Time, which don't include Duration --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-datatype-joda</artifactId> <version>2.0.1</version> </dependency>