Type info is not serialized when a @JsonTypeInfo element is a part of a collection or map

6,542 views
Skip to first unread message

Lev Kuznetsov

unread,
Dec 13, 2016, 11:21:50 AM12/13/16
to jackson-user
I saw https://github.com/FasterXML/jackson-databind/issues/336 and in the end they've repeatedly asked to take this here, so here I am. TLDR - if you have an abstract class (or interface) with @JsonTypeInfo(...) and put a bunch of those into a collection the type information of each element will not be serialized when you serialize the collection (and same thing with maps). I don't understand the reasoning given in the ticket - this is only an issue on serialization when the implementation information is clearly available. You have concrete implementation class of the object to serialize since you've obviously managed to create the object. Of course you know all of its superclasses and implemented interfaces and whatever annotations come with those. I say again, this is only an issue on serialization, if you write JSON for a collection with type information in each element it will get deserialized no problem without any kind of hacks as expected. Here's an example:

package foo;


import java.util.Map;


import com.fasterxml.jackson.annotation.JsonGetter;

import com.fasterxml.jackson.annotation.JsonProperty;

import com.fasterxml.jackson.annotation.JsonTypeInfo;

import com.fasterxml.jackson.annotation.JsonTypeInfo.As;

import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;

import com.fasterxml.jackson.databind.ObjectMapper;


public class Test {


  @JsonTypeInfo (include = As.PROPERTY, use = Id.CLASS)

  public static abstract class Foo {

    @Override

    public String toString () {

      return "." + this.getClass ().getSimpleName ();

    }


    //@JsonGetter ("@class")

    //private String type () {

    //  return getClass ().getName ();

    //}

  }


  public static class Bar extends Foo {

    private @JsonProperty String lol = "lol";

  }


  private Map<String, Foo> foo;

  private Map<String, Bar> bar;


  public static void main (String[] args) throws Exception {

    ObjectMapper m = new ObjectMapper ();

    Foo f = new Bar ();

    System.out.println ("1) " + m.writeValueAsString (f));

    System.out.println ("2) " + m.readValue ("{\"@class\":\"foo.Test$Bar\"}", Foo.class));

    Object o = m.readValue ("{\"b\":{\"@class\":\"foo.Test$Bar\"}}",

                            m.constructType (Test.class.getDeclaredField ("bar").getGenericType ()));

    o = m.readValue ("{\"f\":{\"@class\":\"foo.Test$Bar\"}}",

                     m.constructType (Test.class.getDeclaredField ("foo").getGenericType ()));

    System.out.println ("3) " + o);

    System.out.println ("4) " + m.writeValueAsString (o));

    System.out.println ("5) " + m.readValue (m.writeValueAsString (o),

                                             m.constructType (Test.class.getDeclaredField ("foo").getGenericType ())));

  }

}


The last sysout does not work unless you comment in the @JsonGetter (the hack to make it work) but then the type information appears twice if you just serialize the object itself - not as being part of a collection

Tatu Saloranta

unread,
Dec 13, 2016, 11:31:08 AM12/13/16
to jackso...@googlegroups.com
Quick answer is that you are probably hitting this:

https://github.com/FasterXML/jackson-databind/issues/1410

and the underlying problem here is that you have both "type" metadata
property (default for `@JsonTypeInfo`) and data property "type".
Either you should not add data property at all (comment out, remove),
or you should declare inclusion type as `As.EXISTING_PROPERTY`. In
latter case polymorphic handling code knows not to generate type id
itself, but rather just use value of `type` property.

Ideally it would be possible to avoid double serialization (hence
#1410), but since `TypeSerializer` wraps regular data
`JsonSerializer`, two are only very loosely connected: and
specifically latter has no way to know what former has output. And
conversely when constructing `TypeSerializer`, details of subtype
serializer(s) are not known at all (same type serializer used for all
subtypes of a given basetype).

-+ 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.

Lev Kuznetsov

unread,
Dec 13, 2016, 11:41:26 AM12/13/16
to jackson-user
Instinctively I'd like to avoid having the @JsonGetter method at all as I feel the @JsonTypeInfo should do everything for me, afterall it does on the deserialization. I'm fine with including it as As.EXISTING_PROPERTY since it produces the correct output and input but I feel this is a workaround.

Tatu Saloranta

unread,
Dec 13, 2016, 2:05:12 PM12/13/16
to jackso...@googlegroups.com
Just to make sure: you absolutely do not need such POJO property;
Jackson does not need it.
Since `type` is piece of metadata it does exist in json but isn't (by
default) assumed to be needed as payload data.

So if you don't need it, just drop the getter; value should be written
by default as type id.

Use of EXISTING_PROPERTY was added to allow alternatively to consider
type id as both metadata and real data; it is bit unwieldy -- ideally
it would just auto-detect -- but for now the only reliable way.
Perhaps in future we can improve this to have smoother handling.

I hope this helps!

-+ Tatu +-

Lev Kuznetsov

unread,
Dec 13, 2016, 2:53:17 PM12/13/16
to jackson-user
Try the code I pasted without the getter, it won't serialize type information if the object is a value in a map I'm trying to serialize (and same as part of a collection). The output for the code I pasted above is

1) {"@class":"foo.Test$Bar","lol":"lol"}

2) .Bar

3) {f=.Bar}

4) {"f":{"lol":"lol"}}

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (END_OBJECT), expected FIELD_NAME: missing property '@class' that is to contain type id  (for class foo.Test$Foo)


Where the first line is serialization of the object itself, the fourth line is serialization of a map mapping the string "f" to the same object, notice how "@class" property is missing, followed by exception where it cannot deserialize this json back into a map.

Tatu Saloranta

unread,
Dec 14, 2016, 11:29:16 PM12/14/16
to jackso...@googlegroups.com
The reason for (4) is the good old Java Type Erasure.
What you writing out is essentially `Map<?,?>`, as far as available
type information tells -- there is nothing that could tell otherwise.

So as general rule:

- Do not serialize generic values as root value, if possible.
o but if you do, you MUST provide type information separately
o or, sub-class generic type to make it non-generic

Like so:

// instead of using reflection, construct as `TypeReference`
final TypeReference<?> fooType = new TypeReference<Map<String, Foo>>() { };
// (or using `TypeFactory.constructMapType(...)`)

// important: since root value is generic, MUST provide extra info to avoid
// it being seen as `Map<?,?>`
String json = m.writerFor(fooType)
.writeValueAsString(o);
// after which read succeeds:
System.out.println ("5) " + m.readValue(json, fooType));

Or, with a work-around involving sub-classing to get non-generic type:

static class FooMap extends HashMap<String,Foo> { }

and you do NOT need to specify type for serialization because type
information is now available via super-type declaration (so it's in
class definition; runtime type is still type-erased but that does not
matter).

I hope this helps,

-+ Tatu +-

Lev Kuznetsov

unread,
Dec 15, 2016, 11:13:59 AM12/15/16
to jackson-user
But why would the type erasure on the map be the deciding factor here? The type of the value is known at runtime, it is Bar, it has a property lol - that gets serialized no problem, clearly it knows the type and therefore can see the @JsonTypeInfo annotation on the superclass. I don't have to do objectMapper.writerFor(Foo.class).writeValueAsString(foo) to get the type information included, it is included by calling objectMapper.writeValueAsString(foo) directly. I think for the most part I don't understand this inconsistency. What does it matter that now it's a part of a collection?

Thanks for taking the time with me on this. At this point between you showing me the typed writer and looking at source for resteasy-jackson2-provider, which is what I'll be using to build something that matters, I think everything should work as it's supposed to and I won't need the getter. I suppose this is an artifact of how I set up my prototype.

Tatu Saloranta

unread,
Dec 15, 2016, 12:26:38 PM12/15/16
to jackso...@googlegroups.com
On Thu, Dec 15, 2016 at 8:13 AM, Lev Kuznetsov
<lev.v.k...@gmail.com> wrote:
> But why would the type erasure on the map be the deciding factor here? The
> type of the value is known at runtime, it is Bar, it has a property lol -
> that gets serialized no problem, clearly it knows the type and therefore can
> see the @JsonTypeInfo annotation on the superclass. I don't have to do
> objectMapper.writerFor(Foo.class).writeValueAsString(foo) to get the type
> information included, it is included by calling
> objectMapper.writeValueAsString(foo) directly. I think for the most part I
> don't understand this inconsistency. What does it matter that now it's a
> part of a collection?

Because base type must be known, and same for all values to serialize.
This is necessary due to round-tripping more problem for
deserialization than serialization.
And from performance perspective having to do all very expensive
lookups on every element for every serialization would be impractical.
So it is done once wrt TypeSerializer, and this must be available from
static information.

Another way to think of it is that this is what Jackson requires for
polymorphic type handling: content serialization can use dynamic type
(within limitations of generics), but type id serialization not.

> Thanks for taking the time with me on this. At this point between you
> showing me the typed writer and looking at source for
> resteasy-jackson2-provider, which is what I'll be using to build something
> that matters, I think everything should work as it's supposed to and I won't
> need the getter. I suppose this is an artifact of how I set up my prototype.

Thank you for going through the details and willingness to understand.
This is a complicated area of handling unfortunately... and
implementation as well, not the least because polymorphism support was
added in 1.5, not part of 1.0.

Good luck with the prototype,

-+ Tatu +-
Reply all
Reply to author
Forward
0 new messages