Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Jackson polymorphic serialization not work in lists?

Tags:

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?

like image 858
monitorjbl Avatar asked Dec 10 '15 03:12

monitorjbl


People also ask

Does Jackson use serializable?

Note that Jackson does not use java. io. Serializable for anything: there is no real value for adding that. It gets ignored.

Does Jackson serialize getters?

Jackson doesn't (by default) care about fields. It will simply serialize everything provided by getters and deserialize everything with a matching setter.

What is Jackson object serialization?

Jackson is a solid and mature JSON serialization/deserialization library for Java. The ObjectMapper API provides a straightforward way to parse and generate JSON response objects with a lot of flexibility. This article discussed the main features that make the library so popular.

Does Jackson ObjectMapper use reflection?

Jackson is a framework that provides the means to read and write objects to and from JSON. Typically, it tries to use built in support for common classes and simple types. It will also use a default serializer based on reflection to write the JSON, but will need some guidance for more complex custom objects.

Which fields of the class will be serialized to JSON?

Out of the four fields of the class, just the public booleanValue will be serialized to JSON by default: 3. A Getter Makes a Non-Public Field Serializable and Deserializable

How do I serialize a non-public field in Java?

Now, another simple way to make a field – especially a non-public field – serializable, is to add a getter for it: public class MyDtoWithGetter { private String stringValue; private int intValue; public String getStringValue() { return stringValue; } }

How to prevent password information from being serialized to JSON in Salesforce?

To get there, we simply add the @JsonIgnore annotation on the getter of the password, and enable deserialization for the field by applying the @JsonProperty annotation on the setter: Now the password information won’t be serialized to JSON:

How does JUnit serialization / deserialization work?

A Truck instance is mapped to the named type Truck for example, And It runs a JUnit which checks the serialization / deserialization produces exactly the same object. If we take a look at the produced Json, it looks like the following: Jackson has added a @type attribute to each vehicle json.


2 Answers

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"}] 
like image 88
Sotirios Delimanolis Avatar answered Oct 29 '22 18:10

Sotirios Delimanolis


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;   } } 
like image 26
monitorjbl Avatar answered Oct 29 '22 17:10

monitorjbl