29

Jackson is doing something truly bizarre and I cannot find any explanation for it. I'm doing polymorphic serialization and it works perfectly when an object is on its own. But if you put the same object into a list and serialize the list instead, it erases the type information.

The fact that it's losing type info would lead one to suspect type erasure. But this is happening during serialization of the contents of the list; all Jackson has to do is inspect the current object it's serializing to determine its type.

I've created an example using Jackson 2.5.1:

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.ArrayList;
import java.util.List;

public class Test {

  @JsonIgnoreProperties(ignoreUnknown = true)
  @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
  @JsonSubTypes({
    @Type(value = Dog.class, name = "dog"),
    @Type(value = Cat.class, name = "cat")})
  public interface Animal {}

  @JsonTypeName("dog")
  public static class Dog implements Animal {
    private String name;

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }
  }

  @JsonTypeName("cat")
  public static class Cat implements Animal {
    private String name;

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }
  }

  public static void main(String[] args) throws JsonProcessingException {
    List<Cat> list = new ArrayList<>();
    list.add(new Cat());
    System.out.println(new ObjectMapper().writeValueAsString(list));
    System.out.println(new ObjectMapper().writeValueAsString(list.get(0)));
  }
}

Here's the output:

[{"name":null}]
{"@type":"cat","name":null}

As you can see, Jackson is not adding the type information when the object is in a list. Does anyone know why this is happening?

monitorjbl
  • 4,280
  • 3
  • 36
  • 45

6 Answers6

31

The various reasons for why this happens are discussed here and here. I don't necessarily agree with the reasons, but Jackson, because of type erasure, doesn't off the bat know the type of elements the List (or Collection or Map) contains. It chooses to use a simple serializer that doesn't interpret your annotations.

You have two options suggested in those links:

First, you can create a class that implements List<Cat>, instantiate it appropriately and serialize the instance.

class CatList implements List<Cat> {...}

The generic type argument Cat is not lost. Jackson has access to it and uses it.

Second, you can instantiate and use an ObjectWriter for the type List<Cat>. For example

System.out.println(new ObjectMapper().writerFor(new TypeReference<List<Cat>>() {}).writeValueAsString(list));

will print

[{"@type":"cat","name":"heyo"}]
Sotirios Delimanolis
  • 274,122
  • 60
  • 696
  • 724
  • 6
    Also you can use like `new ObjectMapper().writerFor( mapper.getTypeFactory().constructCollectionType(List.class, Animal.class)).writeValueAsString(list)` – Tom Sebastian Dec 10 '15 at 05:34
  • 2
    That is really weird, and I can't imagine why they would make that choice. Regardless of the type of the list, you'd think the objects that are inside it would be serialized the same way all the time. – monitorjbl Dec 10 '15 at 13:32
  • mapper.writerWithType(mapper.getTypeFactory().constructCollectionType(List.class, Animal.class)).writeValueAsString(list) – Youness Jul 11 '17 at 21:25
16

The answer Sotirios Delimanolis gave is the correct one. However, I thought it'd be nice to post this workaround as a separate answer. if you are in an environment in which you cannot change the ObjectMapper for each type of thing you need to return (like a Jersey/SpringMVC webapp), there is an alternative.

You can simply include a private final field on the class that contains the type. The field won't be visible to anything outside the class, but if you annotate it with @JsonProperty("@type") (or "@class" or whatever your type field is named) Jackson will serialize it regardless of where the object is located.

@JsonTypeName("dog")
public static class Dog implements Animal {
  @JsonProperty("@type")
  private final String type = "dog";
  private String name;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }
}
monitorjbl
  • 4,280
  • 3
  • 36
  • 45
5

I faced this problem as well and this is the workaround that I prefer (I'm using Kotlin but with Java it's pretty much the same)

The parent class configures @JsonTypeInfo to use an existing property as the marker to break the ambiguity of the sub types

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY)
@JsonSubTypes(
        JsonSubTypes.Type(value = Bob::class, name = "bob"),
        JsonSubTypes.Type(value = Alice::class, name = "alice")
)
abstract class Person {

    abstract val jacksonMarker: String
        @JsonProperty("@type")
        get

    // ... rest of the class
}

The subclasses:

class Bob: Person {

    override val jacksonMarker: String
        get() = "bob"

    // ... rest of the class

}


class Alice: Person {

    override val jacksonMarker: String
        get() = "alice"

    // ... rest of the class

}

And you're set.

Giordano
  • 1,401
  • 15
  • 26
  • @saw303 if you serialize a list of `Person` you will get a list of objects with the `@type` discriminator property, which will allow you to correctly deserialize them. I'm not sure why you're saying that this does not answer the question, I've been using code like this in production for quite some time. – Giordano Jun 19 '19 at 08:52
  • ah, now I understand. Maybe you add this additional explanation to your answer. That was not clear to me when I read your answer. – saw303 Jun 20 '19 at 07:20
  • This works for me in Java. Do not forget to add `, name = "bob"` in the annotation to make the link with the `@JsonProperty("@type")` getter. – ARno Nov 05 '20 at 11:03
2

Similar but a little simpler than @monitorjbl above.

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = NAME, include = PROPERTY, property = "type")
@JsonSubTypes({
  @Type(value = Dog.class, name = "dog"),
  @Type(value = Cat.class, name = "cat")})
public interface Animal {}

public static class Dog implements Animal {
  private final String type = "dog";
}

public static class Cat implements Animal {
  private final String type = "cat";
}
William
  • 20,150
  • 8
  • 49
  • 91
1

Sotirios Delimanolis answer is correct. If you are using Kotlin you can simply create a new type like this:

class CatList: List<Cat> by listOf()
Fred
  • 484
  • 8
  • 16
-2

As arrays do not use type erasure you can solve it overriding the ObjectMapper.writeValueAsString to transform the Collection into an Array.

public class CustomObjectMapper extends ObjectMapper {    
    @Override
        public String writeValueAsString(Object value) throws JsonProcessingException {
                //Transform Collection to Array to include type info
                if(value instanceof Collection){
                    return super.writeValueAsString(((Collection)value).toArray());
                }
                else 
                    return super.writeValueAsString(value);
            }
}

Use it into your Test.main:

System.out.println(new CustomObjectMapper().writeValueAsString(list));

Output (cats and dogs list):

[{"@type":"cat","name":"Gardfield"},{"@type":"dog","name":"Snoopy"}]
pdorgambide
  • 1,787
  • 19
  • 33