Now that Jackson 1.5
is out (ready for download?),
it's good to start reviewing some candies from the goody bag. While 1.5
is, technically speaking, a "minor" release it is nothing but grand
judging by number of added features .
In a way Jackson mirrors
development of JDK: Jackson version 1.2 and 1.5 have contained tons of
new stuff, whereas 1.3 and 1.4 have been more about incremental steady
improvements and bug fixes.
Let's start with the top requested new feature: Polymorphic
Type Handling.
1. Polymorphic Types: not just for serialization any more!
First things first: polymorphic types just refer to cases where there is
a single declared property type (say, Animal), but multiple concrete
subtypes (like Dog and Cat). Basic OO stuff, implemented in Java by
sub-classes and interface implementation. Nothing unusual there.
Challenge
is actually that of mapping this cleanly to data formats: as with
relational model (SQL) and hierarchic model (XML), JSON has no native
concept of inheritance (XML has neither, in itself; XML Schema does add
support however).
Jackson has actually supported serialization of all types, including
polymorphic subtypes, since first data binding enabled versions (0.9.5).
This because Jackson by default uses actual runtime type when
determining serializer to use (although it can be configured to only
static, declared type, too) -- which is different from what JAXB does --
and thus has no trouble writing out all properties sub-type has.
But the problem is, without additional type information, it is not
possible to determine intended subtype when deserializing, when there
are multiple subtypes to choose from: when property has declared type of
'Animal', there is no way to know whether it should be reconstructed as
"Dog" or "Cat". This is where PTH implementation comes in.
Obvious answer is that additional type information must be added in
serialized JSON data to allow this later reconstruction. But there are
multiple considerations as to how this should be done, including:
-
What kind of type information should be included (Java class name?
Logical type name? something else?)
-
How (where) should this type information be included (as a property?
using wrapper constructs?)
-
Do we always have to include this type information? (doing so adds
unnecessary bloat in cases where type information is redundant)
Jackson 1.5 tackles these (and some other minor concerns) in an
efficient, configurable, and I hope, elegant way.
2. Enough theory: show me the code!
Ok: chances are that you are not all THAT interested in details of how
implementation was tricky, or what philosophical considerations went on
in determining what to implement and how. So let's start with some toy
code.
Considering case of a polymorphic class; say... Person base class, and
Employee and Customer sub-classes (I have written an "Animal example" at
FX wiki already, let's get some variety), and using a wrapper class such
as:
public class Contact {
public Person initiator; // who initiated contact
public Person receiver; // with whome?
}
and Person (etc) classes as follows
public abstract class Person {
String name;
}
public class Employee {
long employeeId;
}
public class Customer {
String ssn;
ServiceLevel serviceLevel;
}
What do we need to do to make Contact properly deserializable? By
default, no additional type information would be added, and
deserialization would fail
There are two main ways to do this, covering slightly different use
cases, and offering different levels of configurability.
3. Type Info, method #1: explicit annotations
First method is to add explicit @JsonTypeInfo annotation in the declared
type (or its supertypes):
@JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.PROPERTY, property="@class")
public abstract class Person {
//...
}
which would basically enable addition of type information for any
instances of Person, as well as its subtypes. With above configuration
-- include fully-qualified Java class name as type identifier; include
it as main-level additional (meta-)property using property name "@class"
-- output might look something like:
{
"@class" : "com.fasterxml.sample.Employee",
"name" : "Billy Bacon",
"employeeId" : 126509
}
(with additional type information bolded)
So far so good: it is bit extra work to sprinkle annotations (which can
be added via mix-ins, of course), but nothing too bad. As long as you
know types in advance. And you can configure handling pretty well (check
out Javadocs for
more details for @JsonTypeInfo, related; or if I get that far, later
entries on advanced topics)
4. Type info, method #2: default typing
But there is another way, which requires much less code, although
offering bit less configurability. Specifically, you can enable
so-called "default typing" for ObjectMapper by:
mapper.enableDefaultTyping(); // defaults for defaults (see below); include as wrapper-array, non-concrete types
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_OBJECT); // all non-final types
which would then apply default type information (type id is always
fully-qualified Java class; inclusion method is customizable but
defaults to "wrapper array") to all types (classes) that fulfill
applicability criteria: by default it would be Object.class and
non-concrete types (abstract classes, interfaces). So in our case this
would work, given that Person is an abstract class: and if it wasn't, we
could use "ObjectMapper.DefaultTyping.NON_FINAL" to include type
information on all non-final classes.
5. Is that All?
No, not at all. This is just the simplest way to start safely handling
polymorphic types like, say, List<Object>.
What else there is to find is good subject for follow-up entries, but
here are some teasers:
-
Java class name is not a good choice when you have non-Java clients or
services to talk to -- if so, you can use "logical type name" (see
@JsonTypeName and @JsonSubTypes for more info); much like what JAXB
does. Or, even further, you can have fully customized type id
converters! (as long as they can convert to/from JSON Strings)
-
If you don't like using main-level property names (maybe there's a
conflict), you can use other styles; "wrapper Object", "wrapper Array"
-
If you like default typing, but don't like its selection criteria, you
can create your own: just sub-class default TypeResolverBuilder,
assign it to your ObjectMapper.
-
Similarly, you can define fully custom type information handler, using @JsonTypeResolver
annotation -- this will be nicely integrated with the whole system,
you only have to provide actual inclusion, but it will be called for
any existing types.
But enough for now... more on 1.5 later.