Welcome back to the last post in our immutables series! At this point, you’ve made a shift to using the Immutables library. If you missed our earlier posts,

  • Part 1 introduced how to create and use generated immutable objects, using Immutables.
  • Part 2 covered some useful features from the Immutables library, specifically covering different types of attributes and ways of creating immutable instances.
  • Part 3 showed some advanced styling and usage patterns that allow your immutables to be more stylistically homogenous with your existing code base.

In this post, we’ll tackle a new challenge: how do you serialize and deserialize your models? Specifically, how do you serialize to and from JSON?

In Java, there are two main JSON serialization/deserialization libraries—Jackson and Gson. When you get them working properly, they’re quite straightforward to use. For example, to deserialize with Gson, you’d call:

gson.fromJson(myJsonString, MyDesiredClass.class);

However, they’re only straightforward to use when you get them working properly. Let’s take a look at how we can configure our immutables to play nicely with Gson.

The Pain Point

To use Gson, you have to teach it how to serialize or deserialize your object. For basic objects, meaning primitives or bags of primitives, Gson just works. You can also get it working out of the box with nested (static) classes, but that’s roughly it.

At some point, however, you will have some kind of object that Gson cannot handle out of the box. Gson won’t, for example, properly deserialize generics (e.g. any collection) by default.

This is handled by explicitly telling Gson how to serialize and/or deserialize things. The are a few ways to do this:

  • TypeToken
    The typical way of telling Gson how to deserialize a generic type.
  • JsonSerializer
    This involves implementing a class that effectively says when I have an X, this is how you turn it into json.
  • JsonDeserializer
    Same thing, but from json to your object.
  • TypeAdapter
    Basically, it serializes AND deserializes.

You have to create one of these objects, then register it in a Gson builder. This is often tedious and scary—people often don’t want to muck around with low-level serialization/deserialization stuff, they just want to get back to their normal coding.

Immutables & Gson

In a nutshell, this is how we hook up our immutables to Gson:

  1. Add the Immutables-Gson module.
  2. Slap a few configuration-type things on our models.
  3. Immutables-Gson will generate type adapters for us. Register these with Gson.

That’s it! Unless your models are doing something crazy, you won’t need anything more than this.

Add Immutables-Gson Module

Your immutable base class will need the org.immutables:gson module. It will also need Gson. You’ll want to add both of these to your project.

Configure Models

To configure your existing immutable model, all you have to do is one thing—add an annotation to it! You want to add the @Gson.TypeAdapters annotation. That’s it.

@Built
@Gson.TypeAdapters
@Value.Immutable
public abstract class Color {
    public abstract String getRgb();

    // ...
}

This annotation tells our Immutables annotation processor to also generate type adapters for the Color class. These type adapters are what Gson needs to understand how to convert JSON to and from a Color.

If our model’s attribute names don’t align with our expected JSON fields, we can explicitly specify what the fields should be named. This is done with the @SerializedName annotation. This annotation can also be used to specify field names for enum values.

Right now, our RGB field is currently named rgb. If we’d like it to be all uppercase, we can specify that:

@Built
@Gson.TypeAdapters
@Value.Immutable
public abstract class Color {
    @SerializedName("RGB")
    public abstract String getRgb();

    // ...
}

If you also use sandwich style, you might have noticed something weird here—where’s the sandwich? It turns out that we don’t need the builder if we’re just trying to deserialize JSON. If we need the builder in the future, it wouldn’t be hard to come back and add it in.

Finally, it’s possible that you have some weird configuration/requirement and your models don’t serialize the way you want them to. This is probably related to how you’ve defined/designed your models. This sometimes needs a custom serializer/deserializer, but often you can get around it by changing your model structure.

Register Type Adapters

The last step is to register your automatically-generated type adapters.

protected GsonBuilder GsonBuilder() {
    GsonBuilder builder = new GsonBuilder();

    // Register Gson type adapters for immutables
    ServiceLoader typeAdapterFactories = 
        ServiceLoader.load(TypeAdapterFactory.class);
    for (TypeAdapterFactory factory : typeAdapterFactories) {
        builder.registerTypeAdapterFactory(factory);
    }

    return builder;
}

That’s all there is to it!