Immutables, Part 4

Immutables, Part 4

July 22, 2019

At this point, you’ve made a shift to an immutables world. All of your models are immutable (where appropriate) and you’re benefiting from not having to worry about random code messing with your shit. You’ve also started using non-vanilla generated objects and a custom style in order to cut down on boilerplate (e.g. ImmutableFoo.copyOf) and make immutables feel like other objects (e.g. protobufs).

Now, you face a new challenge: how do you serialize and deserialize your model? 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. In this post, we’ll be looking at how to configure our immutables to work well with Gson, allowing for easy JSON serialization and deserialization.

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 shitty because it’s often tedious and it’s often 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.

Let Your Tools Do The Work

Before we actually start talking about how to get Gson working, let’s take a step back—does a solution exist?

Our typical problem is that we have some immutable models and we want to convert to/from JSON. Our serialization and deserialization is pretty straightforward—fields are usually mapped 1:1 and have similar nesting structures.

Serialization/deserialization is a pretty common thing, and a modelling library that doesn’t make it easy to convert to/from JSON seems like it’s lacking a core feature. This is also surely something that other developers need to use. Gson/Jackson are pretty common libraries for this, so we’re hoping that our modelling library either:

  • Can directly serialize to/deserialize from JSON.
  • Can hook up to a popular JSON serialization/deserialization library, such as Gson or Jackson.

This is what we’d expect a solution to look like. Does it exist? The answer is yes.

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!

comments powered by Disqus