Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to de/serialize map itself polymorphic in jackson?

I've searched a lot and only find questions about polymorphic deserialization on the content inside a map. Is it possible to polymorphic deserializing the map itself?

For example, I have a Book class contains a Map as a member variable.

public class Book {
    @JsonProperty
    private Map<String, Object> reviews;

    @JsonCreator
    public Book(Map<String, Object> map) {
        this.reviews = map;
    }
}

Another class have a list of Book class.

public class Shelf {

    @JsonProperty
    private List<Book> books = new LinkedList<>();

    public void setBooks(List<Book> books) {
        this.books = books;
    }

    public List<Book> getBooks() {
       return this.books;
    }
}

And a test class. One book's review map is a Hashtable and another book's review map is a HashMap.

public class Test {

    private Shelf shelf;

    @BeforeClass
    public void init() {
        Map<String, Object> review1 = new Hashtable<>(); // Hashtable here
        review1.put("test1", "review1");
        Map<String, Object> review2 = new HashMap<>(); // HashMap here
        review2.put("test2", "review2");

        List<Book> books = new LinkedList<>();
        books.add(new Book(review1));
        books.add(new Book(review2));
        shelf = new Shelf();
        shelf.setBooks(books);
    }

    @Test
    public void test() throws IOException{
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
//        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        String json = mapper.writeValueAsString(shelf);
        System.out.println(json);

        Shelf sh = mapper.readValue(json, Shelf.class);
        for (Book b : sh.getBooks()) {
            System.out.println(b.getReviews().getClass());
        }
    }
}

The test output

{
  "name" : "TestShelf",
  "books" : [ {
    "reviews" : {
      "test1" : "review1"
    }
  }, {
    "reviews" : {
      "test2" : "review2"
    }
  } ]
}
class java.util.LinkedHashMap
class java.util.LinkedHashMap

The serialization works fine. But after deserialization, both review1 and review2 are LinkedHashMap. I want review1 and review2 to be their actual types which are Hashtable to review1 and HashMap to review2. Is there any way to achieve this?

I don't want to use mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); because it will add the type info for all json properties in the json message. And if there is any better way to do it I don't want to use customized deserializer either. Thanks in advance.

like image 315
Andrew Liu Avatar asked Nov 09 '22 04:11

Andrew Liu


1 Answers

I posted the question on Jackson user forum and they suggest to customized the TypeResolverBuilder and set it in the ObjectMapper instance.

ObjectMapper.setDefaultTyping(...)

My customized TypeResolverBuilder is below and it solved my problem.

public class MapTypeIdResolverBuilder extends StdTypeResolverBuilder {

    public MapTypeIdResolverBuilder() {
    }

    @Override
    public TypeDeserializer buildTypeDeserializer(DeserializationConfig config,
                                                  JavaType baseType, Collection<NamedType> subtypes) {
        return useForType(baseType) ? super.buildTypeDeserializer(config, baseType, subtypes) : null;
    }

    @Override
    public TypeSerializer buildTypeSerializer(SerializationConfig config,
                                              JavaType baseType, Collection<namedtype> subtypes) {
        return useForType(baseType) ? super.buildTypeSerializer(config, baseType, subtypes) : null;
    }

    /**
     * Method called to check if the default type handler should be
     * used for given type.
     * Note: "natural types" (String, Boolean, Integer, Double) will never
     * use typing; that is both due to them being concrete and final,
     * and since actual serializers and deserializers will also ignore any
     * attempts to enforce typing.
     */
    public boolean useForType(JavaType t) {
        return t.isMapLikeType() || t.isJavaLangObject();
    }
}

This solution requires both server side and client side to use the customized TypeResolverBuilder. I know it is not ideal, but it is the best solution I found so far. The details of the solution can be found in this post on my blog.

like image 106
Andrew Liu Avatar answered Nov 14 '22 21:11

Andrew Liu