Immutables

JSON serialization

Overview

It’s not uncommon to use immutable object as messages or documents to transfer or store data. JSON is a simple and flexible format. Moreover, using libraries like Jackson, you can use various additional textual and binary formats: Smile, BSON, CBOR, YAML… etc.

Immutables JSON integration underwent an overhaul for 2.0. This made integration a lot less exotic and more comprehensible.

Instead of the old generated marshaler infrastructure based on Jackson streaming (jackson-core), two new integrations available:

Jackson ——-

Overall Jackson doesn’t require any serious code generation to be flexible and highly performant on the JVM. No additional dependencies are required except for Immutables processor and Jackson library. It is recommended to use Jackson version 2.4+, but earlier versions can work also.

Integration works by generating @JsonCreator factory method and puts @JsonProperty annotations on immutable implementations. To enable this, you should use @JsonSerialize or @JsonDeserialize annotation. Point to an immutable implementation class in as annotation attribute:

import com.fasterxml.jackson.annotation.*;
import org.immutables.value.Value;

@Value.Immutable
@JsonSerialize(as = ImmutableVal.class)
@JsonDeserialize(as = ImmutableVal.class)
interface Val {
  int a();
  @JsonProperty("b") String second();
}

While ImmutableVal may not be generated yet, the above will compile properly. as = ImmutableVal.class attribute have to be added if your codebase predominantly use abstract value as a canonical type, if you mostly use immutable type, then it’s not required to use as attribute. You can use @JsonProperty to customize JSON field names. You can freely use any other facilities of the Jackson library if applicable.

ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(
    ImmutableVal.builder()
        .a(1)
        .second("B")
        .build());
{ "a": 1,
  "b": "B" }

Recently (since 2.3.7) we’ve added an alternative way of Jackson integration using builder. It works when you point @JsonDeserialize(builder) to a generated or “extending” builder. The example will make it clear:

@Value.Immutable
@Value.Style(builder = "new") // builder has to have constructor
@JsonDeserialize(builder = ImmutableVal.Builder.class)
interface Val {
  int a();
  @JsonProperty("b") String second();
}

// or using extending builder
@Value.Immutable
@JsonDeserialize(builder = Val.Builder.class)
interface Val {
  int a();
  @JsonProperty("b") String second();
  class Builder extends ImmutableVal.Builder {}
}

Using the approach shown above, generated builders will have attributes annotated with @JsonProperty so deserialization will work properly.

Things to be aware of

Jackson-Guava

If you use Guava, make sure to use the special serialization module jackson-datatype-guava.

<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-guava</artifactId>
  <version>2.4.0</version>
</dependency>

ObjectMapper mapper = new ObjectMapper();
// register module with object mapper
mapper.registerModule(new GuavaModule());

Jackson and Java 8

For Java 8 specific datatypes use jackson-datatype-jdk8 module.

<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jdk8</artifactId>
  <version>2.6.3</version>
</dependency>
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new Jdk8Module());

Sometimes you might use high-level application framework which handles Jackson for you. So in order to register modules you need to get to ObjectMapper during initialization phase and configure it. Here’s the sample of how it looks like for Dropwizard

public void run(AppConfiguration configuration, Environment environment) throws Exception {
  environment.getObjectMapper().registerModule(new GuavaModule());
  environment.getObjectMapper().registerModule(new Jdk8Module());
  ...
}

Reducing annotation clutter

You can find some number of examples using Immutables and Jackson where annotations are cluttering definitions, annotations are piling on top of each other in many levels. But there are ways to significantly reduce annotation noise. Firstly, if using meta-annotated custom style annotation, you can move it to top level class, package (in corresponding pacakge-info.java file) or even parent package. Secondly, if you mostly use generated value type, rather that abstract value type, you don’t need specify @JsonDeserialize(as = ImmutableForEverySingleValue.class), you can put @JsonSerialize as meta-annotation as with style annotation, and you can put custom (meta-annotated) annotation and put it on a type or a package.

Here’s modified example taken from Lagom Framework documentation Immutable.html page.

@JsonSerialize // Jackson automatic integration, why not?
@Value.Style(
    typeAbstract = "Abstract*",
    typeImmutable = "*",
    visibility = ImplementationVisibility.PUBLIC)
@interface MyStyle {} // Custom style
// ...

@MyStyle //<-- Meta annotated with @JsonSerialize and @Value.Style
// and applies to nested immutable objects
interface BlogEvent extends Jsonable {

  @Value.Immutable // <-- looks a lot cleaner, 1 annotation instead of 3
  interface AbstractPostAdded extends BlogEvent {
    String getPostId();
    BodyChanged getContent();
  }

  @Value.Immutable
  interface AbstractBodyChanged extends BlogEvent {
    @Value.Parameter
    String getBody();
  }

  @Value.Immutable
  interface AbstractPostPublished extends BlogEvent {
    @Value.Parameter
    String getPostId();
  }
}


Gson —-

Dependencies

Gson integration requires the com.google.gson:gson compile and runtime modules. The org.immutables:gson module contains compile-time annotations to generate TypeAdapter factories. Optionally, the org.immutables:gson module can also be used at runtime to enable the following functionality:

<dependency>
  <groupId>org.immutables</groupId>
  <artifactId>gson</artifactId>
  <version>2.10.1</version>
  <!-- If you don't need runtime capabilities - make it compile-only
  <scope>provided</scope>-->
</dependency>
<dependency>
  <groupId>org.immutables</groupId>
  <artifactId>value</artifactId>
  <version>2.10.1</version>
  <scope>provided</scope>
</dependency>

Can’t wait to see generated code?

Generating Type Adapters

Use the annotation @org.immutables.gson.Gson.TypeAdapters to generate a TypeAdapaterFactory implementation which produces adapters to any immutable classes enclosed by @Gson.TypeAdapters annotations. The annotation can be placed on top-level type or package (using package-info.java). The type adapter factory will support all immutable classes in the corresponding type (directly annotated and all nested immutable values) or package. A class named GsonAdapters[NameOfAnnotatedElement] will be generated in the same package.

// generated GsonAdaptersAdapt factory will handle all immutable types here:
// Adapt, Inr, Nst
@Gson.TypeAdapters
@Value.Immutable
public interface Adapt {
  long id();
  Optional<String> description();
  Set<Inr> set();
  Multiset<Nst> bag();

  @Value.Immutable
  public interface Inr {
    int[] arr();
    List<Integer> list();
    Map<String, Nst> map();
    SetMultimap<Integer, Nst> setMultimap();
  }

  @Value.Immutable
  public interface Nst {
    int value();
    String string();
  }
}

Type Adapter registration

Type adapter factories are generated in the same package and registered statically as service providers in META-INF/services/com.google.gson.TypeAdapterFactory. You can manually register factories with GsonBuilder, but the easiest way to register all such factories is by using java.util.ServiceLoader:

import com.google.gson.GsonBuilder;
import com.google.gson.Gson;
import com.google.gson.TypeAdapterFactory;
import java.util.ServiceLoader;
...

GsonBuilder gsonBuilder = new GsonBuilder();
for (TypeAdapterFactory factory : ServiceLoader.load(TypeAdapterFactory.class)) {
  gsonBuilder.registerTypeAdapterFactory(factory);
}

// Manual registration is also an option
gsonBuilder.registerTypeAdapterFactory(new GsonAdaptersMyDocument());

Gson gson = gsonBuilder.create();

String json = gson.toJson(
    ImmutableValueObject.builder()
        .id(1)
        .name("A")
        .build());
// { "id": 1, "name": "A" }

Things to be aware of

JAX-RS integration

A JAX-RS message body reader/writer is provided out of the box. In itself it is a generic Gson integration provider, but it has following special capabilities:

To use immutable types in your JAX-RS services, use org.immutables.gson.stream.GsonMessageBodyProvider which implements javax.ws.rs.ext.MessageBodyReader and javax.ws.rs.ext.MessageBodyWriter. Also do not forget to specify an “application/json” content type, so that the provider will match.

// Contrived illustration for marshaling of immutable abstract types: InputValue and OutputValue
// using GsonMessageBodyProvider
@Path("/test")
public class TestResource {
  @POST
  @Consumes(MediaType.APPLICATION_JSON)
  @Produces(MediaType.APPLICATION_JSON)
  public OutputValue post(InputValue input) {
    int val = input.inputAttribute();
    return ImmutableOutputValue.builder()
       .outputAttribute(val);
       .build();
  }
  ...
}

While this provider can be picked up automatically from the classpath using META-INF/services/javax.ws.rs.ext.* by the JAX-RS engine, sometimes you’ll need to add it manually.

// Dropwizard Application example
@Override
public void run(DwConfiguration configuration, Environment environment) throws Exception {
  environment.jersey().register(new TestResource());
  environment.jersey().register(new GsonMessageBodyProvider());
}

You can create a customized GsonMessageBodyProvider instance:

new GsonMessageBodyProvider(
    new GsonProviderOptionsBuilder()
        .gson(new Gson()) // build custom Gson instance using GsonBuilder methods
        .addMediaTypes(MediaType.TEXT_PLAIN_TYPE) // specify custom media types
        .allowJackson(false) // you can switch off Gson-Jackson bridge
        .lenient(true) // you can enable non-strict mode
        .build()) {
// Some JAX-RS implementations (Jersey) track message body providers by class identity,
// Anonymous class could be defined to create unique class.
// It allows to register couple of GsonMessageBodyProvider with different configuration
// by having unique classes.
};

Mapping Features

Automatically generated bindings are straightforward and generally useful.

While there’s certain amount of customization (like changing field names in JSON), the basic idea is to have direct and straightforward mappings to JSON derived from the structure of value objects, where value objects are adapted to a representation rather than free-form objects having complex mappings to JSON representations.

To add custom binding for types, other than immutable values, use Gson APIs. Please refer to Gson reference

Generic parameters

Generic parameters are supported when some upper level JSON document object specify actual type parameters, so nested document value objects that are parametrized may know exact type used in the context.

Let’s say you have value type Val with the following definition.

@Gson.TypeAdapters
@Value.Immutable
interface Val<T> {
  T val();
  Optional<String> description();
}

You cannot serialize parametrized type to or from JSON without specifying actual type parameters. But if we put parametrized type in a context document which provide actual type parameters we can use our generated type adapters for JSON conversion.

@Gson.TypeAdapters
@Value.Immutable
interface Doc {
  Val<String> str(); // actual type T is String
  Val<Integer> inte(); // actual type T is Integer
  Val<Boolean> bool();  // actual type T is Boolean
}
{
  "str": { "val": "StringValue" },
  "ints": { "val": 124, "description": "Just a number" },
  "bool": { "val": true, "description": "Just a boolean" },
}

Actual type parameters might not only be simple types as string, numbers or booleans, but also nested documents or arrays or the mentioned types.

Note: If generic attributes contain comples nested type variables (think Set<List<T>>), then special routines that extract actual type parameters will be referenced in a generated source code, so you will need org.immutable:json artifact packaged as part of your application, not a compile-only dependency

Field names

By default, the JSON field name is the same as the attribute name. However, it is very easy to specify the JSON field name as it should appear in the JSON representation. Use the value attribute of the com.google.gson.annotations.SerializedName annotation placed on an attribute accessor.

@Value.Immutable
@Gson.TypeAdapters
public abstract class ValueObject {
  @SerializedName("_id")
  public abstract long getId();
  @SerializedName("name")
  public abstract String getNamedAs();
  public abstract int getOtherAttribute();
}

ValueObject valueObject =
    ImmutableValueObject.builder()
        .id(1123)
        .namedAs("Valuable One")
        .otherAttribute(0)
        .build();

valueObject will be marshaled as:

{
  "_id": 1123,
  "name": "Valuable One",
  "otherAttribute": 0
}

@Gson.Named is deprecated in favor of Gson’s SerializedName annotation. As of Gson v2.5 SerializedName is applicable to methods and renders Immutables’ custom annotation unnecessary. In addition to that, there’s support for SerializedName.alternate attribute which allows to specify alternative names used during deserialization.

When running on an Oracle JVM, there’s an option to enable field naming strategy support. Use @Gson.TypeAdapters(fieldNamingStrategy = true) to enable generation of code which uses a field naming strategy. See Javadoc for Gson.TypeAdapters#fieldNamingStrategy. This feature is not supported on Android and other Java runtimes because of heavy use of reflection hacks to workaround Gson’s limitations to make this work.

Ignoring attributes

Collection, optional and default attributes can be ignored during marshaling by using @Gson.Ignore annotation.

Omitting empty fields

Use Gson’s configuration GsonBuilder.serializeNulls() to include empty optional and nullable fields as null. By default those will be omitted, this generally helps to keep JSON clean and to reduce its size if there are a lot of optional attributes. If you want to omit empty collection attributes in the same way as nullable fields — use @Gson.TypeAdapters(emptyAsNulls = true)

@Value.Immutable
@Gson.TypeAdapters(emptyAsNulls = true)
interface Omits {
  Optional<String> string();
  List<String> strings();
}

String json = gson.toJson(ImmutableOmits.builder().build());
// omits all empty
{ }
// with GsonBuilder.serializeNulls()
{ "string": null,
  "strings": [] }

Tuples of constructor arguments

One of the interesting features of Immutables JSON marshaling is the ability to map tuples (triples and so on) of constructor arguments. While not universally useful, some data types could be compactly represented in JSON as array of values. Consider, for example, spatial coordinates or RGB colors.

In order to marshal object as tuple, you need to annotate constructor arguments and disable generation of builders.

@Value.Immutable(builder = false)
@Gson.TypeAdapters
public interface Coordinates {
  @Value.Parameter double latitude();
  @Value.Parameter double longitude();
}

...
Coordinates coordinates = ImmutableCoordinates.of(37.783333, -122.416667);

coordinates will be marshaled as a JSON array rather than a JSON object

[37.783333, -122.416667]

A special case of this are values with single constructor parameter. Having a tuple of 1 argument is essentially equivalent to having just a single argument. Therefore you can marshal and unmarshal such objects as a value of its single argument. If you want to make value to be a wrapper type (for the purposes of adding type-safety), but nevertheless invisible in BSON, you can define it as having no builder and single argument constructor, so that it will become a pure wrapper:

@Gson.TypeAdapters
interface WrapperExample {
  // Name will become wrapper around name string, invisible in JSON
  @Value.Immutable(builder = false)
  interface Name {
    @Value.Parameter String value();
  }

  // Id will become wrapper around id number, invisible in JSON
  @Value.Immutable(builder = false)
  interface Id {
    @Value.Parameter int value();
  }

  @Value.Immutable(builder = false)
  interface Val {
    Id id();
    Name name();
  }
}

Val val = ImmutableVal.build()
  .id(ImmutableId.of(124))
  .name(ImmutableName.of("Nameless"))
  .build();
{
  "id": 124,
  "name": "Nameless"
}

This makes it possible to achieve the desired level of abstraction and type safety without cluttering JSON data structure.

Polymorphic mapping

An interesting feature of Immutables Gson marshaling is the ability to map an abstract type to one of it’s subclasses by structure as opposed to by a “discriminator” field.

Define a common supertype class and subclasses, then use @org.immutables.gson.Gson.ExpectedSubtypes annotation to list the expected subtypes. Then, you can use a supertype in an attribute as a plain reference or as a collection.

@Gson.ExpectedSubtypes can be placed on:

@Value.Immutable
@Gson.TypeAdapters
public interface HostDocument {
  // Host document contain list of values
  // @Gson.ExpectedSubtypes annotation could be also placed on attribute.
  List<AbstractValue> value();

  @Gson.ExpectedSubtypes({
    InterestingValue.class,
    RelevantValue.class
  })
  public interface AbstractValue {}

  @Value.Immutable
  public interface InterestingValue extends AbstractValue {
    int number();
  }

  @Value.Immutable
  public interface RelevantValue extends AbstractValue {
    String string();
  }
}
{
  "values": [
    { "number": 2 },
    { "string": "Relevant?" },
    { "number": 1 },
  ]
}

As you can guess, the above JSON fragment may be deserialized to HostDocument, the value attribute of which will contain instances of InterestingValue, followed by RelevantValue, and finally InterestingValue.

In addition, when using a value nested in enclosing, the exact set of subclasses can be figured out from the set of nested types in the enclosing scope. In that case, the @Gson.ExpectedSubtypes annotation may have its “value” attribute omitted.

@Gson.TypeAdapters
@Value.Enclosing
interface Enc {
  interface A {}
  @Value.Immutable interface B extends A { int b(); }
  @Value.Immutable interface C extends A { double c(); }
  @Value.Immutable interface E {
    @Gson.ExpectedSubtypes A a(); // B and C will be discovered
  }
}

Although a nice feature, you should generally avoid the use of polymorphic marshaling if performance is important. Current implementation may suffer JIT deoptimizations due to exceptions being thrown and caught during regular deserialization. This renders the polymorphic deserialization feature useful for auxiliary uses (such as configuration or model serialization), but less useful for high-throughput document streaming. However, the implementation can be changed (improved) in future.

Things to be aware of

Gson-Jackson bridge

We can push Gson’s performance to its limits by delegating low-level streaming to Jackson. Gson is pretty optimized in itself, but Jackson is playing an “unfair game” by optimizing the whole chain of JSON streaming, including UTF-8 encoding handling, recycling of special buffers, DIY number parsing and formatting etc. This can be as much as 200% faster for some workloads.

There’s sample benchmark which we used only to see relative difference. As usual, take those numbers with a grain of salt: it’s just some numbers for some JSON documents on some MacBook.

Benchmark                                             Mode  Samples     Score     Error  Units
o.i.s.j.JsonBenchmarks.autoJackson                    avgt        5   709.249 ±  19.170  us/op
o.i.s.j.JsonBenchmarks.immutablesGson                 avgt        5  1155.550 ±  48.843  us/op
o.i.s.j.JsonBenchmarks.immutablesGsonJackson          avgt        5   682.605 ±  20.839  us/op
o.i.s.j.JsonBenchmarks.pojoGson                       avgt        5  1402.759 ± 101.077  us/op
o.i.s.j.JsonBenchmarks.pojoGsonJackson                avgt        5   935.107 ±  58.210  us/op
o.i.s.j.JsonBenchmarks.pojoJackson                    avgt        5   721.767 ±  47.782  us/op

It is possible use Gson to serialize to and from various additional textual and binary serialization formats supported by Jackson:

Smile, BSON, CBOR, YAML… etc.