Feature: Contextual Handlers (serializers, deserializers)
(see Jira entry JACKSON-385 for details)
Problem to Solve
The main goal of Jackson 1.7 was the extensibility. One of limitations for existing functionality for registering custom serializers and deserializers was that a single handler instance is used for all instances of a property, so it is not possible to have contextual behavior. Contextual behavior here means that serialization or deserialization can be done differently depending on which property (or root value) is being processed.
One practical example is that of modifying behavior of serialization with annotations added next to getter method or field that is used to access value to serialize. By default there isn't enough information to access such annotations; and even if this was possible, performance penalty could be significant.
Solution
Solution implemented here is as follows:
Two new interfaces were added: ContextualSerializer and ContextualDeserializer
If a JsonSerializer implements ContextualSerializer, or JsonDeserializer implements ContextualDeserializer, matching "contextualizer" method is called after serializer/deserializer has been constructed (and resolved if necessary).
"Contextualizer" method is passed a BeanProperty object that contains information about logical property being handled; and specifically including:
- Accessor to access annotations associated with the property
- Accessor to access annotations from the context of the property: context usually refers to Class that contains property (class where field or method is declared; or in case of base classes, sub-class that is being handled)
This allows customizing handling on per-property basis, while still maintaining good performance: annotation lookups are only done when constructing handler instance and not when they are used.
Example
Let's assume that we want to define an annotation that can be used to override java.util.DateFormat used for serializing java.util.Date values:
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation // important so that it will get included!
public @interface CustomDateFormat {
public String value();
}so that we could annotate a Bean like so:
public class MyBean {
@CustomDateFormat("YYYY-MM-DD")
public java.util.Date getCreationDate();
}so serializer we want could be implemented something like:
public class ContextualDateSerializer
extends JsonSerializer<Date>
implements ContextualSerializer<Date>
{
protected final DateFormat format;
public AnnotatedContextualSerializer() { this(null); }
public AnnotatedContextualSerializer(String formatStr) {
format = (format == null) ? null new SimpleDateFormat(formatStr);
}
@Override
public void serialize(Date date, JsonGenerator jgen, SerializerProvider provider) throws IOException
{
// null means "just use default"; provider has functionality to uyse
if (format == null) {
provider.defaultSerializeDateValue(date, jgen);
} else {
jgen.writeString(format.format(date));
}
}
@Override
public JsonSerializer<String> createContextual(SerializationConfig config, BeanProperty property)
throws JsonMappingException
{
// First find annotation used for getter or field:
CustomDateFormat ann = property.getAnnotation(CustomDateFormat.class);
if (ann == null) { // but if missing, default one from class
ann = property.getContextAnnotation(CustomDateFormat.class);
}
// If no customization found, just return base instance (this); no need to construct new serializer
String format = (ann == null) ? null : ann.value();
if (ann == null || ann.length() == 0) {
return this;
}
return new ContextualDateSerializer(format);
}Given all these pieces, how would serializer be registered? With 1.7, preferred mechanism would be to implement a Module; for example:
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("EnhancedDatesModule", new Version(0, 1, 0, "alpha"));
module.addSerializer(java.util.Date.class, new ContextualDateSerializer());
mapper.registerModule(module);