114

I have a problem deserializing a json string with Gson. I receive an array of commands. The command can be start, stop , some other type of command. Naturally I have polymorphism, and start/stop command inherit from command.

How can I serialize it back to the correct command object using gson?

Seems that I get only the base type, that is the declared type and never the runtime type.

C. Ross
  • 31,137
  • 42
  • 147
  • 238
Sophie
  • 1,580
  • 3
  • 16
  • 20

9 Answers9

127

This is a bit late but I had to do exactly the same thing today. So, based on my research and when using gson-2.0 you really don't want to use the registerTypeHierarchyAdapter method, but rather the more mundane registerTypeAdapter. And you certainly don't need to do instanceofs or write adapters for the derived classes: just one adapter for the base class or interface, provided of course that you are happy with the default serialization of the derived classes. Anyway, here's the code (package and imports removed) (also available in github):

The base class (interface in my case):

public interface IAnimal { public String sound(); }

The two derived classes, Cat:

public class Cat implements IAnimal {

    public String name;

    public Cat(String name) {
        super();
        this.name = name;
    }

    @Override
    public String sound() {
        return name + " : \"meaow\"";
    };
}

And Dog:

public class Dog implements IAnimal {

    public String name;
    public int ferocity;

    public Dog(String name, int ferocity) {
        super();
        this.name = name;
        this.ferocity = ferocity;
    }

    @Override
    public String sound() {
        return name + " : \"bark\" (ferocity level:" + ferocity + ")";
    }
}

The IAnimalAdapter:

public class IAnimalAdapter implements JsonSerializer<IAnimal>, JsonDeserializer<IAnimal>{

    private static final String CLASSNAME = "CLASSNAME";
    private static final String INSTANCE  = "INSTANCE";

    @Override
    public JsonElement serialize(IAnimal src, Type typeOfSrc,
            JsonSerializationContext context) {

        JsonObject retValue = new JsonObject();
        String className = src.getClass().getName();
        retValue.addProperty(CLASSNAME, className);
        JsonElement elem = context.serialize(src); 
        retValue.add(INSTANCE, elem);
        return retValue;
    }

    @Override
    public IAnimal deserialize(JsonElement json, Type typeOfT,
            JsonDeserializationContext context) throws JsonParseException  {
        JsonObject jsonObject = json.getAsJsonObject();
        JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
        String className = prim.getAsString();

        Class<?> klass = null;
        try {
            klass = Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new JsonParseException(e.getMessage());
        }
        return context.deserialize(jsonObject.get(INSTANCE), klass);
    }
}

And the Test class:

public class Test {

    public static void main(String[] args) {
        IAnimal animals[] = new IAnimal[]{new Cat("Kitty"), new Dog("Brutus", 5)};
        Gson gsonExt = null;
        {
            GsonBuilder builder = new GsonBuilder();
            builder.registerTypeAdapter(IAnimal.class, new IAnimalAdapter());
            gsonExt = builder.create();
        }
        for (IAnimal animal : animals) {
            String animalJson = gsonExt.toJson(animal, IAnimal.class);
            System.out.println("serialized with the custom serializer:" + animalJson);
            IAnimal animal2 = gsonExt.fromJson(animalJson, IAnimal.class);
            System.out.println(animal2.sound());
        }
    }
}

When you run the Test::main you get the following output:

serialized with the custom serializer:
{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Cat","INSTANCE":{"name":"Kitty"}}
Kitty : "meaow"
serialized with the custom serializer:
{"CLASSNAME":"com.synelixis.caches.viz.json.playground.plainAdapter.Dog","INSTANCE":{"name":"Brutus","ferocity":5}}
Brutus : "bark" (ferocity level:5)

I've actually done the above using the registerTypeHierarchyAdapter method too, but that seemed to require implementing custom DogAdapter and CatAdapter serializer/deserializer classes which are a pain to maintain any time you want to add another field to Dog or to Cat.

Massimiliano Kraus
  • 3,638
  • 5
  • 27
  • 47
Marcus Junius Brutus
  • 26,087
  • 41
  • 189
  • 331
  • 1
    Just a little addition: To make it work, make sure your classes are are NOT defined internally (sub-class). – waqaslam Mar 15 '13 at 16:46
  • 6
    Note that serialization of class names and deserialization (from user input) using Class.forName can present security implications in some situations, and is thus discouraged by the Gson dev team. https://code.google.com/p/google-gson/issues/detail?id=340#c2 – Programmer Bruce Apr 01 '13 at 04:58
  • 4
    How did you manage not to get an infinite loop in serialization, you're calling context.serialize(src); which will be invoking your adapter again. This is what happened in my similar code. – che javara Nov 27 '13 at 22:11
  • @chejavara I think it should be `context.serialize(src, src.getClass());` – Dag Nov 28 '13 at 11:30
  • 6
    Wrong. This solution does not work. If you call context.serialize in any way you will end up with infinite recursion. I wonder why people post without actually testing code. I tried with 2.2.1. See the bug described in http://stackoverflow.com/questions/13244769/gson-polymorphic-serialization – che javara Nov 28 '13 at 15:30
  • @chejavara The solution was obviously tested as I couldn't just conceive the code right out of my head. After your comment I tried it again (copying code out of the answer and only adding import statements) and works fine (I tested it with the following dependency: ). If your problem persists I guess I could upload the solution to a github repo for you. – Marcus Junius Brutus May 06 '14 at 13:38
  • @MarcusJuniusBrutus that may mean it works with 2.0 but not with 2.2.1 - any chance you could retry your test with 2.2.1 ? – che javara May 06 '14 at 20:49
  • @chejavara just tried and verified it works with 2.2.1 as well. – Marcus Junius Brutus May 06 '14 at 21:53
  • @chejavara and the github repo as promised: https://github.com/mperdikeas/json-polymorphism.git – Marcus Junius Brutus May 06 '14 at 22:04
  • 4
    @MarcusJuniusBrutus I ran your code and it seems it works only in this special case - because you have defined a superinterface IAnimal, and IAnimalAdapter uses it. If you instead only had 'Cat' then you will get the infinite recursion problem. So this solution still does not work in the general case - only when you are able to define a common interface. In my case there was no interface so I had to use a different approach with TypeAdapterFactory. – che javara May 07 '14 at 15:05
  • 2
    User src.getClass().getName() instead of src.getClass().getCanonicalName(). This meads the code will work for inner / nested classes as well. – mR_fr0g Jan 30 '15 at 11:57
  • 1
    I used this method, but to prevent infinite recursion I created a new Gson instance without the type adapter and used that to deserialize. – Tunji_D Sep 01 '15 at 15:45
  • Doesn't work for Root object, see https://github.com/google/gson/issues/712 (the same issue) – ultraon Oct 15 '15 at 15:59
  • 1
    @mR_fr0g yeap done that and updated the github repo with the sample code too. – Marcus Junius Brutus Oct 15 '15 at 16:18
  • This is kind of useless, since you need to specify the type in the call. gsonExt.toJson(animal, IAnimal.class) If you call gsonExt.toJson(animal) It doesn't work. This specifically mean that if my animal is part of another structure, it won't be serialized correctly – ModdyFire Oct 27 '17 at 03:37
  • I am using Gson 2.7 and @Tunji_D 's suggestion works. Use a new Gson instance without type adapter for "inner" serialization and deserialization. This is for the case where you need to serialize the object of root classes as well. – Waqas Ilyas Jun 21 '18 at 22:45
  • Additional note: If you have to use this for multiple classes, you can make things even easier by making the adapter generic. – Namnodorel Sep 20 '18 at 17:53
  • Ran into the serialization-recursion-issue in 2021 - and solved it using [`public JsonElement serialize(Object src, Type typeOfSrc);`](https://github.com/google/gson/blob/gson-parent-2.8.7/gson/src/main/java/com/google/gson/JsonSerializationContext.java#L48) with `Object.getClass()` as the second parameter instead of `src.getClass()` – Sebastian Schmitt Aug 13 '21 at 21:24
13

Gson currently has a mechanism to register a Type Hierarchy Adapter that reportedly can be configured for simple polymorphic deserialization, but I don't see how that's the case, as a Type Hierarchy Adapter appears to just be a combined serializer/deserializer/instance creator, leaving the details of instance creation up to the coder, without providing any actual polymorphic type registration.

It looks like Gson will soon have the RuntimeTypeAdapter for simpler polymorphic deserialization. See http://code.google.com/p/google-gson/issues/detail?id=231 for more info.

If use of the new RuntimeTypeAdapter isn't possible, and you gotta use Gson, then I think you'll have to roll your own solution, registering a custom deserializer either as a Type Hierarchy Adapter or as Type Adapter. Following is one such example.

// output:
//     Starting machine1
//     Stopping machine2

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
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 Foo
{
  // [{"machine_name":"machine1","command":"start"},{"machine_name":"machine2","command":"stop"}]
  static String jsonInput = "[{\"machine_name\":\"machine1\",\"command\":\"start\"},{\"machine_name\":\"machine2\",\"command\":\"stop\"}]";

  public static void main(String[] args)
  {
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    CommandDeserializer deserializer = new CommandDeserializer("command");
    deserializer.registerCommand("start", Start.class);
    deserializer.registerCommand("stop", Stop.class);
    gsonBuilder.registerTypeAdapter(Command.class, deserializer);
    Gson gson = gsonBuilder.create();
    Command[] commands = gson.fromJson(jsonInput, Command[].class);
    for (Command command : commands)
    {
      command.execute();
    }
  }
}

class CommandDeserializer implements JsonDeserializer<Command>
{
  String commandElementName;
  Gson gson;
  Map<String, Class<? extends Command>> commandRegistry;

  CommandDeserializer(String commandElementName)
  {
    this.commandElementName = commandElementName;
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
    gson = gsonBuilder.create();
    commandRegistry = new HashMap<String, Class<? extends Command>>();
  }

  void registerCommand(String command, Class<? extends Command> commandInstanceClass)
  {
    commandRegistry.put(command, commandInstanceClass);
  }

  @Override
  public Command deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
      throws JsonParseException
  {
    try
    {
      JsonObject commandObject = json.getAsJsonObject();
      JsonElement commandTypeElement = commandObject.get(commandElementName);
      Class<? extends Command> commandInstanceClass = commandRegistry.get(commandTypeElement.getAsString());
      Command command = gson.fromJson(json, commandInstanceClass);
      return command;
    }
    catch (Exception e)
    {
      throw new RuntimeException(e);
    }
  }
}

abstract class Command
{
  String machineName;

  Command(String machineName)
  {
    this.machineName = machineName;
  }

  abstract void execute();
}

class Stop extends Command
{
  Stop(String machineName)
  {
    super(machineName);
  }

  void execute()
  {
    System.out.println("Stopping " + machineName);
  }
}

class Start extends Command
{
  Start(String machineName)
  {
    super(machineName);
  }

  void execute()
  {
    System.out.println("Starting " + machineName);
  }
}
Programmer Bruce
  • 64,977
  • 7
  • 99
  • 97
  • If you can change APIs, then note that Jackson currently has a mechanism for relatively simple polymorphic deserialization. I posted some examples at http://programmerbruce.blogspot.com/2011/05/deserialize-json-with-jackson-into.html – Programmer Bruce Jun 08 '11 at 06:45
  • [`RuntimeTypeAdapter`](http://code.google.com/p/google-gson/source/browse/trunk/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java) is now complete, unfortunately it doesn't look like it's in Gson core yet. :-( – Jonathan Aug 14 '13 at 12:52
8

Marcus Junius Brutus had a great answer (thanks!). To extend his example, you can make his adapter class generic to work for all types of objects (Not just IAnimal) with the following changes:

class InheritanceAdapter<T> implements JsonSerializer<T>, JsonDeserializer<T>
{
....
    public JsonElement serialize(T src, Type typeOfSrc, JsonSerializationContext context)
....
    public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
....
}

And in the Test Class:

public class Test {
    public static void main(String[] args) {
        ....
            builder.registerTypeAdapter(IAnimal.class, new InheritanceAdapter<IAnimal>());
        ....
}
user2242263
  • 81
  • 1
  • 2
8

Google has released its own RuntimeTypeAdapterFactory to handle the polymorphism but unfortunately it's not part of the gson core (you must copy and paste the class inside your project).

Example:

RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(runtimeTypeAdapterFactory)
    .create();

Here I have posted a full working example of it using the models Animal, Dog and Cat.

I think it's better to rely on this adapter rather than reimplement it from scratch.

db80
  • 4,157
  • 1
  • 38
  • 38
  • For those who want to play with it - it's in maven under `org.danilopianini:gson-extras`. Bad thing is it doesn't work with Kotlin data classes. – expert Oct 23 '20 at 09:46
7

GSON has a pretty good test case here showing how to define and register a type hierarchy adapter.

http://code.google.com/p/google-gson/source/browse/trunk/gson/src/test/java/com/google/gson/functional/TypeHierarchyAdapterTest.java?r=739

To use that do this:

    gson = new GsonBuilder()
          .registerTypeAdapter(BaseQuestion.class, new BaseQuestionAdaptor())
          .create();

Serialize method of the adapter can be a cascading if-else check of what type it is serializing.

    JsonElement result = new JsonObject();

    if (src instanceof SliderQuestion) {
        result = context.serialize(src, SliderQuestion.class);
    }
    else if (src instanceof TextQuestion) {
        result = context.serialize(src, TextQuestion.class);
    }
    else if (src instanceof ChoiceQuestion) {
        result = context.serialize(src, ChoiceQuestion.class);
    }

    return result;

Deserializing is a bit hacky. In the unit test example, it checks for existence of tell-tale attributes to decide which class to deserialized to. If you can change the source of the object you're serializing, you can add a 'classType' attribute to each instance which holds the FQN of the instance class's name. This is so very un-object-oriented though.

k.c. sham
  • 381
  • 2
  • 6
2

Long time has passed, but I couldn't find a really good solution online.. Here is small twist on @MarcusJuniusBrutus's solution, that avoids the infinite recursion.

Keep the same deserializer, but remove the serializer -

public class IAnimalAdapter implements JsonDeSerializer<IAnimal> {
  private static final String CLASSNAME = "CLASSNAME";
  private static final String INSTANCE  = "INSTANCE";

  @Override
  public IAnimal deserialize(JsonElement json, Type typeOfT,
        JsonDeserializationContext context) throws JsonParseException  {
    JsonObject jsonObject =  json.getAsJsonObject();
    JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
    String className = prim.getAsString();

    Class<?> klass = null;
    try {
        klass = Class.forName(className);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
        throw new JsonParseException(e.getMessage());
    }
    return context.deserialize(jsonObject.get(INSTANCE), klass);
  }
}

Then, in your original class, add a field with @SerializedName("CLASSNAME"). The trick is now to initialize this in the constructor of the base class, so make your interface into a an abstract class.

public abstract class IAnimal {
  @SerializedName("CLASSNAME")
  public String className;

  public IAnimal(...) {
    ...
    className = this.getClass().getName();
  }
}

The reason there is no infinite recursion here is that we pass the actual runtime class (i.e. Dog not IAnimal) to context.deserialize. This will not call our type adapter, as long as we use registerTypeAdapter and not registerTypeHierarchyAdapter

Ginandi
  • 843
  • 8
  • 20
2

Updated Answer - Best parts of all other answers

I am describing solutions for various use cases and would be addressing the infinite recursion problem as well

  • Case 1: You are in control of the classes, i.e, you get to write your own Cat, Dog classes as well as the IAnimal interface. You can simply follow the solution provided by @marcus-junius-brutus(the top rated answer)

    There won't be any infinite recursion if there is a common base interface as IAnimal

    But, what if I don't want to implement the IAnimal or any such interface?

    Then, @marcus-junius-brutus(the top rated answer) will produce an infinite recursion error. In this case, we can do something like below.

    We would have to create a copy constructor inside the base class and a wrapper subclass as follows:

.

// Base class(modified)
public class Cat implements IAnimal {

    public String name;

    public Cat(String name) {
        super();
        this.name = name;
    }
    // COPY CONSTRUCTOR
    public Cat(Cat cat) {
        this.name = cat.name;
    }

    @Override
    public String sound() {
        return name + " : \"meaow\"";
    };
}



    // The wrapper subclass for serialization
public class CatWrapper extends Cat{


    public CatWrapper(String name) {
        super(name);
    }

    public CatWrapper(Cat cat) {
        super(cat);
    }
}

And the serializer for the type Cat:

public class CatSerializer implements JsonSerializer<Cat> {

    @Override
    public JsonElement serialize(Cat src, Type typeOfSrc, JsonSerializationContext context) {

        // Essentially the same as the type Cat
        JsonElement catWrapped = context.serialize(new CatWrapper(src));

        // Here, we can customize the generated JSON from the wrapper as we want.
        // We can add a field, remove a field, etc.


        return modifyJSON(catWrapped);
    }

    private JsonElement modifyJSON(JsonElement base){
        // TODO: Modify something
        return base;
    }
}

So, why a copy constructor?

Well, once you define the copy constructor, no matter how much the base class changes, your wrapper will continue with the same role. Secondly, if we don't define a copy constructor and simply subclass the base class then we would have to "talk" in terms of the extended class, i.e, CatWrapper. It is quite possible that your components talk in terms of the base class and not the wrapper type.

Is there an easy alternative?

Sure, it has now been introduced by Google - this is the RuntimeTypeAdapterFactory implementation:

RuntimeTypeAdapterFactory<Animal> runtimeTypeAdapterFactory = RuntimeTypeAdapterFactory
.of(Animal.class, "type")
.registerSubtype(Dog.class, "dog")
.registerSubtype(Cat.class, "cat");

Gson gson = new GsonBuilder()
    .registerTypeAdapterFactory(runtimeTypeAdapterFactory)
    .create();

Here, you would need to introduce a field called "type" in Animal and the value of the same inside Dog to be "dog", Cat to be "cat"

Complete example: https://static.javadoc.io/org.danilopianini/gson-extras/0.2.1/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.html

  • Case 2: You are not in control of the classes. You join a company or use a library where the classes are already defined and your manager doesn't want you to change them in any way - You can subclass your classes and have them implement a common marker interface(which doesn't have any methods) such as AnimalInterface.

    Ex:

.

// The class we are NOT allowed to modify

public class Dog implements IAnimal {

    public String name;
    public int ferocity;

    public Dog(String name, int ferocity) {
        super();
        this.name = name;
        this.ferocity = ferocity;
    }

    @Override
    public String sound() {
        return name + " : \"bark\" (ferocity level:" + ferocity + ")";
    }
}


// The marker interface

public interface AnimalInterface {
}

// The subclass for serialization

public class DogWrapper  extends Dog implements AnimalInterface{

    public DogWrapper(String name, int ferocity) {
        super(name, ferocity);
    }

}

// The subclass for serialization

public class CatWrapper extends Cat implements AnimalInterface{


    public CatWrapper(String name) {
        super(name);
    }
}

So, we would be using CatWrapper instead of Cat, DogWrapper instead of Dog and AlternativeAnimalAdapter instead of IAnimalAdapter

// The only difference between `IAnimalAdapter` and `AlternativeAnimalAdapter` is that of the interface, i.e, `AnimalInterface` instead of `IAnimal`

public class AlternativeAnimalAdapter implements JsonSerializer<AnimalInterface>, JsonDeserializer<AnimalInterface> {

    private static final String CLASSNAME = "CLASSNAME";
    private static final String INSTANCE  = "INSTANCE";

    @Override
    public JsonElement serialize(AnimalInterface src, Type typeOfSrc,
                                 JsonSerializationContext context) {

        JsonObject retValue = new JsonObject();
        String className = src.getClass().getName();
        retValue.addProperty(CLASSNAME, className);
        JsonElement elem = context.serialize(src); 
        retValue.add(INSTANCE, elem);
        return retValue;
    }

    @Override
    public AnimalInterface deserialize(JsonElement json, Type typeOfT,
            JsonDeserializationContext context) throws JsonParseException  {
        JsonObject jsonObject = json.getAsJsonObject();
        JsonPrimitive prim = (JsonPrimitive) jsonObject.get(CLASSNAME);
        String className = prim.getAsString();

        Class<?> klass = null;
        try {
            klass = Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new JsonParseException(e.getMessage());
        }
        return context.deserialize(jsonObject.get(INSTANCE), klass);
    }
}

We perform a test:

public class Test {

    public static void main(String[] args) {

        // Note that we are using the extended classes instead of the base ones
        IAnimal animals[] = new IAnimal[]{new CatWrapper("Kitty"), new DogWrapper("Brutus", 5)};
        Gson gsonExt = null;
        {
            GsonBuilder builder = new GsonBuilder();
            builder.registerTypeAdapter(AnimalInterface.class, new AlternativeAnimalAdapter());
            gsonExt = builder.create();
        }
        for (IAnimal animal : animals) {
            String animalJson = gsonExt.toJson(animal, AnimalInterface.class);
            System.out.println("serialized with the custom serializer:" + animalJson);
            AnimalInterface animal2 = gsonExt.fromJson(animalJson, AnimalInterface.class);
        }
    }
}

Output:

serialized with the custom serializer:{"CLASSNAME":"com.examples_so.CatWrapper","INSTANCE":{"name":"Kitty"}}
serialized with the custom serializer:{"CLASSNAME":"com.examples_so.DogWrapper","INSTANCE":{"name":"Brutus","ferocity":5}}
Manish Kumar Sharma
  • 12,982
  • 9
  • 58
  • 105
1

If you want manage a TypeAdapter for a type and an other for his sub type, you can use a TypeAdapterFactory like this :

public class InheritanceTypeAdapterFactory implements TypeAdapterFactory {

    private Map<Class<?>, TypeAdapter<?>> adapters = new LinkedHashMap<>();

    {
        adapters.put(Animal.class, new AnimalTypeAdapter());
        adapters.put(Dog.class, new DogTypeAdapter());
    }

    @SuppressWarnings("unchecked")
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        TypeAdapter<T> typeAdapter = null;
        Class<?> currentType = Object.class;
        for (Class<?> type : adapters.keySet()) {
            if (type.isAssignableFrom(typeToken.getRawType())) {
                if (currentType.isAssignableFrom(type)) {
                    currentType = type;
                    typeAdapter = (TypeAdapter<T>)adapters.get(type);
                }
            }
        }
        return typeAdapter;
    }
}

This factory will send the most accurate TypeAdapter

r3n0j
  • 71
  • 7
0

If you combine Marcus Junius Brutus's answer with user2242263's edit, you can avoid having to specify a large class hierarchy in your adapter by defining your adapter as working on an interface type. You can then provide default implementations of toJSON() and fromJSON() in your interface (which only includes these two methods) and have every class you need to serialize implement your interface. To deal with casting, in your subclasses you can provide a static fromJSON() method that deserializes and performs the appropriate casting from your interface type. This worked superbly for me (just be careful about serializing/deserializing classes that contain hashmaps--add this when you instantiate your gson builder:

GsonBuilder builder = new GsonBuilder().enableComplexMapKeySerialization();

Hope this helps someone save some time and effort!