How to Serialize and Deserialize Generic Objects using GSON
Introduction
Java is great its Generic collections are awesome! Well not always. The real problem with generics and java before java 1.8 is that you cannot determine the type parameter in a generic class on runtime. That is the main reason why you often see passing XXX.class as a parameter to constructors and methods. I am not going to yell further so lets cut it short and see the problem and solution.
Problem
Lets say we are using command pattern and want the commands to be serialized and deserialized from java object to string and vice versa. GSon (wiki) is a great library for object-to-json transformation. However if the object is a generic object then you have some more coding to do. Here is my example;
Command class which is parent of all commands
public class Command<T> implements Serializable{ public enum State{ NONE, IDLE, PROCESSING, DONE } protected State state = State.NONE; protected Object undoData; public String getName(){ return this.getClass().getSimpleName(); } public State getState() { return state; } public void setState(State state) { this.state = state; } public Object getUndoData() { return undoData; } public void setUndoData(Object undoData) { this.undoData = undoData; } }
Create command to be used for creation of some sort T
public class CreateCommand<T> extends Command<T> { T source; public CreateCommand() { } public CreateCommand(T source) { this.source = source; } public T getSource() { return source; } public void setSource(T source) { this.source = source; } }
MigrateCommand is to be used for migrating some data from source to destination
public class MigrateCommand<T> extends Command<T> { T destination; T source; public MigrateCommand() { } public MigrateCommand(T destination, T source) { this.destination = destination; this.source = source; } public T getDestination() { return destination; } public void setDestination(T destination) { this.destination = destination; } public T getSource() { return source; } public void setSource(T source) { this.source = source; } }
Lets make things even more complicated. We want to have a chain of commands which holds a list of commands of same type where T is the source type for Command and C is the type of command;
public class ChainCommand<T, C> extends Command<T> implements Iterable<C> { List<C> commands = new ArrayList<C>(); Class<C> clazz; public ChainCommand(Class<C> clazz) { this.clazz = clazz; } public ChainCommand() { } public boolean addCommand(C command) { return commands.add(command); } public boolean removeCommand(C command) { return commands.remove(command); } public boolean containsCommand(C command) { return commands.contains(command); } @Override public Iterator<C> iterator() { return commands.iterator(); } public List<C> getCommands() { return commands; } public Class<C> getClazz() { return clazz; } public void setCommands(List<C> commands) { this.commands = commands; } public void setClazz(Class<C> clazz) { this.clazz = clazz; } }
That’s enough complicated for a generic type serialization but seriously this is just a case you may end up struggling in the field.
Now it is time for the meat of the code 🙂 ;
class CommandProcessor{ // this method simply creates the test data and serialize void testSerializeCommands() { List<Command<String>> commands = new ArrayList<Command<String>>(); commands.add(new CreateCommand<String>("CreateSource")); commands.add(new MigrateCommand<String>("MigrateDestination", "MigrateSource")); Command c1 = new CreateCommand<String>("ChainCreate1"); Command c2 = new CreateCommand<String>("ChainCreate2"); ChainCommand chainCommand = new ChainCommand(CreateCommand.class); chainCommand.addCommand(c1); chainCommand.addCommand(c2); commands.add(chainCommand); // CommandSerializer is our class which does the actual serialisation Gson gson = new GsonBuilder().registerTypeAdapter(CommandPersistContainer.class,new CommandSerializer()).create(); ObjectMapper objectMapper = new ObjectMapper(); try { String jsonString = objectMapper.writeValueAsString(commands); System.out.println(jsonString); } catch (JsonProcessingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
Here is the code doing the actual serialization
public class CommandSerializer implements JsonDeserializer<List<Command<String>>>{ private Class getCommandClassForString(String classSimpleName) { if (StringUtils.equalsIgnoreCase(classSimpleName, CreateCommand.class.getSimpleName())) { return new CreateCommand<String>().getClass(); } if (StringUtils.equalsIgnoreCase(classSimpleName, DeleteCommand.class.getSimpleName())) { return new DeleteCommand<String>().getClass(); } if (StringUtils.equalsIgnoreCase(classSimpleName, MigrateCommand.class.getSimpleName())) { return new MigrateCommand<String>().getClass(); } if (StringUtils.equalsIgnoreCase(classSimpleName, UpdateCommand.class.getSimpleName())) { return new UpdateCommand<String>().getClass(); } return null; } private Command<String> deserializeFor(JsonElement je, JsonDeserializationContext context) { JsonObject asJsonObject = je.getAsJsonObject(); String classSimpleName = asJsonObject.get("name").getAsString(); if (StringUtils.equalsIgnoreCase(classSimpleName, CreateCommand.class.getSimpleName())) { return context.<Command<String>>deserialize(je, getCommandClassForString(classSimpleName)); } if (StringUtils.equalsIgnoreCase(classSimpleName, DeleteCommand.class.getSimpleName())) { return context.<Command<String>>deserialize(je, getCommandClassForString(classSimpleName)); } if (StringUtils.equalsIgnoreCase(classSimpleName, MigrateCommand.class.getSimpleName())) { return context.<Command<String>>deserialize(je, getCommandClassForString(classSimpleName)); } if (StringUtils.equalsIgnoreCase(classSimpleName, UpdateCommand.class.getSimpleName())) { return context.<Command<String>>deserialize(je, getCommandClassForString(classSimpleName)); } return null; } @Override public List<Command<String>> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { List<Command<String>> commandList = new ArrayList<Command<String>>(); JsonArray ja = json.getAsJsonArray(); for (JsonElement je : ja) { JsonObject asJsonObject = je.getAsJsonObject(); Command<String> command = deserializeFor(je, context); if (command != null) { commandList.add(command); } else { ChainCommand<String, Command<String>> chainCommand = new ChainCommand<String, Command<String>>(); chainCommand.setState(Command.State.valueOf(asJsonObject.get("state").getAsString())); JsonElement rawUndoData = asJsonObject.get("undoData"); String undoData = rawUndoData.isJsonNull() ? null : rawUndoData.getAsString(); chainCommand.setUndoData(undoData == null ? null : OrganisationUnitBean.State.valueOf(undoData)); JsonArray jsonArray = asJsonObject.get("commands").getAsJsonArray(); for (JsonElement child : jsonArray) { chainCommand.getCommands().add(deserializeFor(child, context)); } if(!chainCommand.getCommands().isEmpty()){ chainCommand.setClazz((Class<Command<String>>) chainCommand.getCommands().get(0).getClass()); } commandList.add(chainCommand); } } return commandList; } }