Gson Polymorphic Deserialization
I am working on a project that requires deserialization of json text and I am using Google gson for it. The problem, however, is that I am using subclassing to be able to perform different operation on slightly different messages. I know the gson people are working on a solution to the problem, but in the mean time I have come up with a somewhat ugly hack that works for me.
package ljos.adapter; import java.lang.reflect.Type; import ljos.message.Message; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; public class MessageDeserializer implements JsonDeserializer<Message> { @Override public Message deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { JsonObject jobj = json.getAsJsonObject(); String type = jobj.get("type").getAsString().toLowerCase(); try { Class<?> c = Class.forName("ljos.message." + Character.toTitleCase(type.charAt(0)) + type.substring(1) + "Message"); return context.deserialize(json, c); } catch (ClassNotFoundException e) { throw new JsonParseException("Unrecognized action type: " + type); } } }
As we can see here I dispatch on a keyword in the json called
type. I have a package that contains all the message types and use
Class.forName()
to find the class. I then use the context, with
that class, to deserialize the message.
I have written some unit tests to ensure that it at least works in the trivial case.
package ljos.test.adapter; import static org.hamcrest.CoreMatchers.hasItems; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import ljos.adapter.MessageDeserializer; import ljos.message.action.Message; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonParseException; @RunWith(JUnit4.class) public class MessageDeserializerTest { private Gson gson; @Before public void initialize() { gson = new GsonBuilder() .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_DASHES) .registerTypeAdapter(Message.class, new MessageDeserializer()) .create(); } @Test public void deserializesMoveMessage() { String json = "{'type':'move', 'direction':'left'}"; Message message = gson.fromJson(json, Message.class); assertTrue(message instanceof MoveMessage); MoveAction action = (MoveMessage)message; assertEquals(action.getType(), "move"); assertEquals(action.getDirection(), "left"); } @Test(expected=JsonParseException.class) public void failsOnDeserializationOnNonsense() { String json = "{'message':'action', 'type':'nonsense'}"; gson.fromJson(json, ActionMessage.class); } }
The Message
and MoveMessage
classes are defined as follows:
package ljos.message; public class Message { protected String type; public String getMessage() { return message; } }
and
package ljos.message; public class MoveMessage extends Message { private String direction; public String getDirection() { return direction; } }