Polymorphic deserialization with @JsonTypeInfo annotations but also considering valueType?

51 views
Skip to first unread message

Jeff Evans

unread,
Jan 28, 2019, 6:36:07 PM1/28/19
to jackson-user
Suppose I have the following class hierarchy and Jackson annotations.

@JsonTypeInfo(use=JsonTypeInfo.Id.CLASS
public class Base {
  private int value;
  // snipped getter and setter for "value" field
}

public class Foo extends Base {
}

public class Bar extends Base {
}

I would like to be able to perform all of the following deserialization cases:

// these both work
Foo foo1 = new ObjectMapper().readValue("{\"@class\": \"Foo\", \"value\": 1}", Base.class)
Foo foo2 = new ObjectMapper().readValue("{\"@class\": \"Foo\", \"value\": 1}", Foo.class)
// so does these
Bar bar1 = new ObjectMapper().readValue("{\"@class\": \"Bar\", \"value\": 1}", Base.class)
Bar bar2 = new ObjectMapper().readValue("{\"@class\": \"Bar\", \"value\": 1}", Bar.class)

// these two fail, with com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (END_OBJECT), expected FIELD_NAME: missing property '@class' that is to contain type id
Foo foo2 = new ObjectMapper().readValue("{\"value\": 1}", Foo.class)
Bar bar2 = new ObjectMapper().readValue("{\"value\": 1}", Bar.class)

Ideally, the last two cases should still work.  Even though the String content does not contain the type ID field (the default of @class in this case), the deserializer should still be able to make use of the fact that I explicitly requested the class in the readValue calls, via the valueType parameter (Foo.class and Bar.class respectively).

Is it possible to configure the deserializer to work in this way?  I read through the Wiki page on polymorphic deserialization a few times but couldn't figure out how to approach this.  I suspect it involves making use of @JsonTypeResolver and/or @JsonTypeIdResolver, but I'm not sure.  Any tips on how to proceed are greatly appreciated.

Tatu Saloranta

unread,
Jan 31, 2019, 1:28:18 AM1/31/19
to jackson-user
This is an open and unsolved issue, basically, and there are a few
requests to support use of explicitly passed type information (at
least in case of root value being polymorphic). It would be good if
things worked as you suggest.

Unfortunately I am not aware of working solution, yet, since although
it is possible to specify "default type" for case of missing type id,
that is considered static so only one default type can be configured
(and not dynamically).

-+ Tatu +-

Bertil Muth

unread,
Jun 11, 2020, 1:45:16 PM6/11/20
to jackson-user
Hi folks, I'm Bertil, I'm new to this group.

I approached this problem from a different angle.

I created a small facade library for Jackson that creates a specific builder for the different cases, like those:

    final ObjectMapper objectMapper =
        json
().property("@class").toSubclassesOf(Base.class).mapper();

   
// these both work
   
Foo foo1 = (Foo) objectMapper.readValue("{\"@class\": \"Foo\", \"value\": 11}", Base.class);
   
Foo foo2 = objectMapper.readValue("{\"@class\": \"Foo\", \"value\": 12}", Foo.class);
   
   
// so do these
   
Bar bar1 = (Bar) objectMapper.readValue("{\"@class\": \"Bar\", \"value\": 21}", Base.class);
   
Bar bar2 = objectMapper.readValue("{\"@class\": \"Bar\", \"value\": 22}", Bar.class);
   
   
// so do these (could have used standard ObjectMapper as well)
   
Foo foo3 = json().mapper().readValue("{\"value\": 13}", Foo.class);
   
Bar bar3 = json().mapper().readValue("{\"value\": 23}", Bar.class);

The classes themselvers are anootation free:
public class Base {
 
private int value;


 
public int getValue() {
   
return value;
 
}

 
public void setValue(int value) {
   
this.value = value;
 
}

 
@Override
 
public String toString() {
   
return "Base [value=" + value + "]";
 
}
}




I'd be really interested in your thoughts and comments.

Tatu Saloranta

unread,
Jun 11, 2020, 2:33:25 PM6/11/20
to jackson-user
On Thu, Jun 11, 2020 at 10:45 AM Bertil Muth <Berti...@hood-group.com> wrote:
>
> Hi folks, I'm Bertil, I'm new to this group.

Hi Bertil. Welcome!
First of all, thank you for sharing this. I think many users could
find this useful.

One question I have is this: do you think it would be easy to make
Moonwlker a regular Jackson module?
Module itself could be built using builder-style construction, and
then registered to ObjectMapper, just like any other module.
While this might be slightly less convenient than building using your
package (and there is also question
of how to handle modules your package depends), there would be some
benefits due to vast number of
other modules Jackson has. For example, this approach to polymorphic
handling could be used with other
Jackson-support dataformats: XML, YAML, CSV, CBOR, Smile etc.
Similarly support for datatype modules (Guava, Joda) could be added.

Or more generally -- since some or all of above is also possible by
just extending Moonwlker builder API --
it'd probably be good to allow full extensibility that Jackson modules
offer, and document that on README.

Anyway, thank you again for sharing this with everyone,

-+ Tatu +-

Bertil Muth

unread,
Jun 12, 2020, 1:25:41 AM6/12/20
to jackson-user
Thanks for the quick reply! Sounds interesting. My hunch would be that for that module, it would be necessary
to get the "owner" ObjectMapper and modify it, but I might me wrong.
Is there any official documentation on Module development, besides the Javadoc of
the Module/SimpleModule classes?

Bertil Muth

unread,
Jun 12, 2020, 2:33:42 AM6/12/20
to jackson-user
The problem is: I provide my own TypeResolverBuilder and TypeIdResolver implementations,
and I don't think there are any methods for this in the Context object.

Tatu Saloranta

unread,
Jun 14, 2020, 1:45:16 PM6/14/20
to jackson-user
On Thu, Jun 11, 2020 at 11:33 PM Bertil Muth <Berti...@hood-group.com> wrote:
>
> The problem is: I provide my own TypeResolverBuilder and TypeIdResolver implementations,
> and I don't think there are any methods for this in the Context object.
>

While this is not explicitly exposed, "owner" that is passed is
guaranteed to be `ObjectMapper`; so

((ObjectMapper) context.getOwner())

gives you essentially full access. This should only be used in cases
where context does not expose necessary extension points,
and I think you are right that this would be one of those cases.

Another thing that I realized is that for Module dependencies, Jackson
2.10 added method `getDependencies()` in `Module`, so you could
actually provide such dependencies automatically (and even make them
configurable via builders, I think, if users wanted to provide
differently configured instances of dependencies).

-+ Tatu +-

Bertil Muth

unread,
Jun 15, 2020, 5:53:13 AM6/15/20
to jackson-user
Thanks a lot.
The new syntax in the v0.0.6 version of Moonwlker
is now like this:

ObjectMapper objectMapper = new ObjectMapper();

MoonwlkerModule module =
 
MoonwlkerModule.builder()
   
.fromProperty("type").toSubclassesOf(Person.class)
   
.build();

objectMapper
.registerModule(module);

More in the spirit of extensibility through modules.
I explain more options on the website.

Tatu Saloranta

unread,
Jun 16, 2020, 1:36:33 PM6/16/20
to jackson-user
Excellent!

I could add a link to this module from main `FasterXML/jackson` repo's
README, although
need to think of how to classify it (under which section, how to describe).

-+ Tatu +-

Bertil Muth

unread,
Jun 16, 2020, 5:12:26 PM6/16/20
to jackson-user
That would be great. Looking at the categories, classification is indeed a bit tricky.
I'd be happy in any case.
Reply all
Reply to author
Forward
0 new messages