ProgrammerBoy
ProgrammerBoy

Reputation: 11

Jackson fasterxml mapper - serialize/deserialize map of list of interface/abstract object type

I have searched and searched but found no answer/solution so am resorting to asking this myself.

Using fasterxml jackson 2.8.7

I have a complex map that I pass to angular and get back via a Spring mapping as a JSON string.

Map:

Map<String, List<TableModel>> tableEntryData = new LinkedHashMap<>();

It is a Map of List of TableModel. Table model is an interface that extends into several concrete classes. So using TableModel as the object type allows me to put whatever concrete class extended from TableModel into the List.

I can pass this to angular fine using:

ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
tableEntryJSON = mapper.writeValueAsString( tableEntryData );

But when I try to deserialize the JSON:

tableEntryData = mapper.readValue(tableEntryJSON, new TypeReference<LinkedHashMap<String, LinkedList<TableModel>>>() {} );

I am facing this exception:

com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.NullPointerException) (through reference chain: java.util.LinkedHashMap["Table_A_List"]->java.util.LinkedList[8])

Now my data Map (tableEntryData) contains data like this:

Map = { [Table_A_List] = {LinkedList of 13 objects of TableA}, [Table_B_List] = {LinkedList of 2 objects of TableB} }

Where Table_A_List and Table_B_List are map keys. TableA and TabelB are classes that implement interface TableModel.

The JSON created is like this:

{   "TABLE_A_List" : [ {
    "deserializeType" : "TableA",
    "amount" : 0.0,   }, {
    "deserializeType" : "TableA",
    "amount" : 8.3,   }, {
    "deserializeType" : "TableA",
    "amount" : 20.0,   }, {
    "deserializeType" : "TableA",
    "amount" : 19.4,   }, {
    "deserializeType" : "TableA",
    "amount" : 33.9,   }, {
    "deserializeType" : "TableA",
    "amount" : 11.3,   }, {
    "deserializeType" : "TableA",
    "amount" : 23.6,   },{
    "deserializeType" : "TableA",
    "amount" : 2.6,   },{
    "deserializeType" : "TableA",
    "amount" : 3.6,   },{
    "deserializeType" : "TableA",
    "amount" : 23.0,   },{
    "deserializeType" : "TableA",
    "amount" : 230.6,   },{
    "deserializeType" : "TableA",
    "amount" : 23.8,   },{
    "deserializeType" : "TableA",
    "amount" : 11.1,   }, ],
"TABLE_B_List" : [ {
    "deserializeType" : "TableB",
    "code" : 9,   }, {
    "deserializeType" : "TableB",
    "code" : 1,   },  ] }

which seems correct.

In order to correctly deserialize concrete classes from the interface, I have defined things as follows

TableModel:

@JsonDeserialize(using = ModelInstanceDeserializer.class) @JsonIgnoreProperties(ignoreUnknown = true) public interface TableModel { .... }

TableA:

@JsonDeserialize(as = TableA.class)
@JsonIgnoreProperties(ignoreUnknown = true)
public class TableA implements Serializable 
{
    String deserializeType = "TableA"; //This is for providing type info that helps with the deserialization since JSON mapper erases type info inside Maps/Lists

//public getters for fields
}

TableB:

@JsonDeserialize(as = TableB.class)
@JsonIgnoreProperties(ignoreUnknown = true)
public class TableB implements Serializable 
{
    String deserializeType = "TableB"; //This is for providing type info that helps with the deserialization since JSON mapper erases type info inside Maps/Lists

//public getters for fields
}

Then I created a deserializer class ModelInstanceDeserializer as well:

import java.io.IOException;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

public class ModelInstanceDeserializer extends JsonDeserializer<TableModel> {
    @Override
    public TableModel deserialize(JsonParser jp, DeserializationContext ctxt)
            throws IOException, JsonProcessingException {
        ObjectMapper mapper = (ObjectMapper) jp.getCodec();
        ObjectNode root = (ObjectNode) mapper.readTree(jp);
        Class<? extends TableModel> instanceClass = null;

        String type = "";
        if (root.has("deserializeType")) {
            type = root.get("deserializeType").asText();
        }

        if (type.equals("TableA")) {
            instanceClass = TableA.class;
        } else if (type.equals("TableA")) {
            instanceClass = TableB.class;
        }
        if (instanceClass == null) {
            return null;
        }
        return mapper.readValue(jp, instanceClass);
    }
}

Now while this baby works somewhat, there is a problem. The deserializer reads the second map entry as a List that is part of the first map entry. What is happening is that the deserializer seems to read two records at a time from the TABLE_A_List JSON and as a result ends up including TABLE_B_List on the 7th entry:

{
  "TABLE_B_List" : [ {
    "deserializeType" : "TableB",
    "code" : 9,
  }, {
    "deserializeType" : "TableB",
    "code" : 1,
  },  ]
}

There are 13 total entries in TABLE_A_List.

So can anybody point me on how to do this correctly. I think I need to configure the deserializer better or something else is the issue.

Yes I can always go back and get rid of the TableModel approach and use something better but that is not the point. It would require too much code change at this point. I need to make this work correctly.

Upvotes: 0

Views: 2051

Answers (1)

ProgrammerBoy
ProgrammerBoy

Reputation: 11

Found the solution a few minutes later despite having lost my brains over this for 2 days.

Use:

return mapper.readValue(root.toString(), instanceClass);

Instead of:

return mapper.readValue(jp, instanceClass);

readValue on JsonParser ends up incrementing the root node. It was a logical error + lack of bloody guides.

Thanks everyone for your answers! :*

Upvotes: 1

Related Questions