Polymorphic deserialization without annotations

986 views
Skip to first unread message

Johan L

unread,
Apr 26, 2017, 1:48:07 PM4/26/17
to jackson-user
We're having problems figuring out how to configure jackson to match how we'd like to have things. We have a bunch of legacy java classes (that we cannot annotate) that we'd like de-serialize from json. We have something similar to the following structure.
interface Animal { ... }

interface Dog implements Animal { ... }
class DogImpl implements Dog { ... }

interface Cat implements Animal { ... }
class CatImpl implements Cat { ... }

class Zoo {
 
List<Animal> animals;
 
String zooName;
}
Preferably we'd like to see jackson understand and convert the following json
{
 
"zooName": "Carl's Zoo",
 
"animals": [
   
{ "@class": "com.acme.zoo.DogImpl", ... },
    { "@class": "com.acme.zoo.DogImpl", ... },
   
{ "@class": "com.acme.zoo.CatImpl", ... },
  ]
}
But we also like to be able to send Lists as root objects, like: 
[
  
{ "@class": "com.acme.zoo.DogImpl", ... },
  { "@class": "com.acme.zoo.DogImpl", ... },
 
{ "@class": "com.acme.zoo.CatImpl", ... },
]

Our current approach is to have a SimpleModule that uses addAbstractTypeMapping between Dog/Cat and DogImpl/CatImpl. And addValueInstantiator to add custom instatiators for CatImpl/DogImpl, because we need to use factories to create the legacy classes.
And we add type info with As.PROPERTY, but none of the different DefaultTyping seems to give us the desired  result. The whole thing is then hooked into Spring via a Converter to map jackson to expected target class (using objectMapper.readValue(.., targetClass)).

Is the above behavior something that jackson can be configured for? Or do we have to implement some custom things and hook into jackson, if so, what would that be? We're running jackson v. 2.5.1 at the moment (it was used in another part of the product).

Tatu Saloranta

unread,
Apr 26, 2017, 2:03:50 PM4/26/17
to jackson-user
On Wed, Apr 26, 2017 at 2:24 AM, Johan L <johan....@gmail.com> wrote:
> We're having problems figuring out how to configure jackson to match how
> we'd like to have things. We have a bunch of legacy java classes (that we
> cannot annotate) that we'd like de-serialize from json. We have something

This is the main case use case that mix-ins were designed for you know.
But assuming that won't work for some other reason, let's see.

> similar to the following structure.
> interface Animal { ... }
>
> interface Dog implements Animal { ... }
> class DogImpl implements Dog { ... }
>
> interface Cat implements Animal { ... }
> class CatImpl implements Cat { ... }
>
> class Zoo {
> List<Animal> animals;
> String zooName;
> }
> Preferably we'd like to see jackson understand and convert the following
> json
> {
> "zooName": "Carl's Zoo",
> "animals": [
> { "@class": "com.acme.zoo.DogImpl", ... },
> { "@class": "com.acme.zoo.DogImpl", ... },
> { "@class": "com.acme.zoo.CatImpl", ... },
> ]
> }
> But we also like to be able to send Lists as root objects, like:
> [
> { "@class": "com.acme.zoo.DogImpl", ... },
> { "@class": "com.acme.zoo.DogImpl", ... },
> { "@class": "com.acme.zoo.CatImpl", ... },
> ]
>
> Our current approach is to have a SimpleModule that uses
> addAbstractTypeMapping between Dog/Cat and DogImpl/CatImpl. And
> addValueInstantiator to add custom instatiators for CatImpl/DogImpl, because
> we need to use factories to create the legacy classes.

That's not really what this facility is meant for: type information
should indicate type, and not be subject to further resolution.
It works well for non-polymorphic cases, but overlaps with PM
handling. So I would not expect that to work here.

> And we add type info with As.PROPERTY, but none of the different
> DefaultTyping seems to give us the desired result. The whole thing is then
> hooked into Spring via a Converter to map jackson to expected target class
> (using objectMapper.readValue(.., targetClass)).

DefaulTyping really is just a way to indicate behavior similar to
`@JsonTypeInfo`, but applied sort of similar to how mix-ins work.
But inclusion done using matching.

> Is the above behavior something that jackson can be configured for? Or do we
> have to implement some custom things and hook into jackson, if so, what
> would that be? We're running jackson v. 2.5.1 at the moment (it was used in
> another part of the product).

Instead of trying to use class names -- which really are meant to be
1-to-1 physical matches, similar to JDK serialization -- it would seem
better to use Type Id (use = Id.NAME). You can then handle translation
between id and actual class in configurable manner (not necessarily
super easily, but you can register handlers).

But I think I am missing something on your use case: who is producing
the JSON? Why wouldn't class name included be the one to use? What are
you trying to change?

-+ Tatu +-

ps. Even if sticking to 2.5.x, you should definitely consider latest
patch version (2.5.5 I think, if not 2.5.6) -- patch versions are safe
upgrades and contain many bug fixes.

Johan L

unread,
Apr 28, 2017, 8:42:34 AM4/28/17
to jackson-user
Thank you for your answers!

We've deliberately avoided mix-ins, so far, mostly because the legacy system (that we're adding rest/json on top of) is quite large, so it would result in quite a lot of mixins needed. And I completely agree with you that using full class names is not an optimal solution (but we're still doing a poc, so its ok for now). If we switch over to JsonTypeInfo.Id.NAME later on, how would you add that (I'm guessing that we need to implement some kind of translator between class and name), if we don't go with mixins or annotations?

We're in full control of the JSON that is sent in. But we choose to send the interface name (com.acme.zoo.Dog) in the json, because there might be different concrete implementations (depending on customer adaptations, hence the factories). But if that conflicts with PM then we have to go with concrete classes until we add id<->class translations. 

Preferably we would only have to add @class to the cases where jackson cannot figure it out based on the target. I.e only for cases where we have fields with interfaces, lists with generics or sub classes.

Right now I'm toying around with the previous code example and enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT, As.PROPERTY), Then serialize it with writeValueAsString and see if I can de-serialize it with readValue. It seems that if animals in Zoo is defined as List<?> jackson adds @class and can de-serialize it. But if its List<Animal> then it doesn't add @class, nor can it de-serialize (even if I manually add @class to the json). How come? :)

Tatu Saloranta

unread,
Apr 28, 2017, 5:25:03 PM4/28/17
to jackson-user
On Fri, Apr 28, 2017 at 5:42 AM, Johan L <johan....@gmail.com> wrote:
> Thank you for your answers!
>
> We've deliberately avoided mix-ins, so far, mostly because the legacy system
> (that we're adding rest/json on top of) is quite large, so it would result
> in quite a lot of mixins needed. And I completely agree with you that using
> full class names is not an optimal solution (but we're still doing a poc, so
> its ok for now). If we switch over to JsonTypeInfo.Id.NAME later on, how
> would you add that (I'm guessing that we need to implement some kind of
> translator between class and name), if we don't go with mixins or
> annotations?

Type id <-> class name translation is configurable; piece that does it
is `TypeIdResolver`, constructed by
`TypeResolverBuilder`. One way to "inject" builder would be overriding

public TypeResolverBuilder<?> findTypeResolver(MapperConfig<?> config,


of AnnotationIntrospector (like JacksonAnnotationIntrospector). Note,
too, that despite name this class does not need to be based on
annotations at all -- it is simply the abstraction to decouple actual
annotations (jackson, jaxb, custom) from configuration actions.

> We're in full control of the JSON that is sent in. But we choose to send the
> interface name (com.acme.zoo.Dog) in the json, because there might be
> different concrete implementations (depending on customer adaptations, hence
> the factories). But if that conflicts with PM then we have to go with
> concrete classes until we add id<->class translations.

If you use type name over class you can send interfaces; this is not
fundamentally problematic.
Although you could consider possibly using simpler ids than interface
classes as well.
But that's your choice.

> Preferably we would only have to add @class to the cases where jackson
> cannot figure it out based on the target. I.e only for cases where we have
> fields with interfaces, lists with generics or sub classes.

The main limitation really is that determination of whether type is
needed has to be static (based on target class hierarchy),
and not dynamic just based on JSON content. There are issues/wishes to
implement basic type discovery, based on (for example)
presence/absence of a property, but no implementation yet. It may be
something to implement in future, but does not exist yet.

With that limit, Jackson does indeed only require (and look for) type
id in cases it considers it necessary.

> Right now I'm toying around with the previous code example and
> enableDefaultTyping(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT,
> As.PROPERTY), Then serialize it with writeValueAsString and see if I can
> de-serialize it with readValue. It seems that if animals in Zoo is defined
> as List<?> jackson adds @class and can de-serialize it. But if its
> List<Animal> then it doesn't add @class, nor can it de-serialize (even if I
> manually add @class to the json). How come? :)

At root value level -- and only there -- type is taken from instance,
and subject to Java Type Erasure.
This means that all generic types, like ArrayList<> etc, will only be
seen as `ArrayList<?>`: parameterization is only available to
compiler. But anything reachable via properties DOES retain
parameters.

My recommendation is to never use generic types as root values: always
POJO (or array).
But if they are used then full type needs to be passed
(ObjectMapper.writerFor(type).....).

This is typically not a problem for deserialization since type must be
passed anyway so callers usually pass full generic type.
It's only for serialization where convenience of passing instance
lures callers into false sense of safety :)

-+ Tatu +-
> --
> You received this message because you are subscribed to the Google Groups
> "jackson-user" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to jackson-user...@googlegroups.com.
> To post to this group, send email to jackso...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.
Reply all
Reply to author
Forward
0 new messages