Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jackson custom deserialization for polymorphic objects

I looked in many questions related to this but i can't find this exact use case. Assumption here is that i am using Java Jackson library.

I have the following class hierarchy:

public class Event    {
    @JsonProperty("si")
    String sessionId;

    @JsonProperty("eventType")
    String eventType;
...
}

@JsonSerialize(include= JsonSerialize.Inclusion.NON_NULL)
public class InitEvent extends Event
{

     @JsonProperty("pm")
     Params params;

     public Params getParams()
     {
          return params;
     }

     .....
}

@JsonSerialize(include= JsonSerialize.Inclusion.NON_NULL)
public class RecoEvent extends Event
{

     @JsonProperty("ti")
     String targetId;

     @JsonProperty("tt")
     int targetType;


     public String getTargetId()
     {
           return targetId;
     }

    ....
}

The rule of deserialization is:

  • If eventType == 0 then deserialize to a InitEvent

  • If eventType == 0 then deserialize to a RecoEvent

Out of the box the Jackson deserialization will not work because it does not know which class to deserialize with. One way to handle that is by doing as follows on the base class:

@JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.PROPERTY, property="@class")
@JsonSerialize(include= JsonSerialize.Inclusion.NON_NULL)
public class Event

The problem of this solution is assumes the client will serialize with the same mapper since the @class element needs to be present now in the JSON.

My client will not send the extra @class element in the incoming JSON.

What is the required solution?

How I could write a custom deserializer that picks the right derived class based on the eventType value?

Thx in advance

like image 473
isaac.hazan Avatar asked Nov 01 '22 14:11

isaac.hazan


1 Answers

At the end I had to write my own deserializer.

This works in 2 steps, first implement the custom deserializer and then register it.

The implementation works as follows:

public class EvtListDeserializer extends JsonDeserializer<EventList>
{
    private static ObjectMapper mapper = new ObjectMapper();

    @Override
    public EventList deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException
    {
        EventList eventList = new EventList();
        List<Event> listOfEvents = new ArrayList<Event>();
        ObjectCodec oc = jsonParser.getCodec();
        JsonNode eventListNode = oc.readTree(jsonParser);
        ArrayNode node = (ArrayNode) eventListNode.get("evt");
        Iterator<JsonNode> events = node.elements();
        for (; events.hasNext(); )
        {
            JsonNode eventNode = events.next();
            int eventType = eventNode.get("type").asInt();

            Event event;
            if (eventType == EventTypes.MoveEvent.getValue())
            {
                event = mapper.readValue(eventNode.toString(), MoveEvent.class);
            }
            else if (eventType == EventTypes.CopyEvent.getValue())
            {
                event = mapper.readValue(eventNode.toString(), CopyEvent.class);
            }
            else
            {
                throw new InvalidEventTypeException("Invalid event type:" + eventType);
            }

            listOfEvents.add(event);
        }

        eventList.setEvents(listOfEvents);

        return eventList;
    }
}

Then you just register it as follows in the class that uses your mapper:

public void init()
{
    SimpleModule usageModule = new SimpleModule().addDeserializer(EventList.class, new EvtListDeserializer());
    mapper.registerModule(usageModule);
}

That works perfectly.

like image 196
isaac.hazan Avatar answered Nov 09 '22 11:11

isaac.hazan