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:
@JsonCreator
, @JsonProperty
annotations and a helper class.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
Value.Style.additionalJsonAnnotations
style attribute to specify such annotation types.@JsonIgnore
, you should explicitly make an attribute non-mandatory. In Immutables, an attribute can be declared as non-mandatory via @Nullable
, Optional
or @Value.Default
which are all different in their effect and we do not derive anything automatically.Value.Style.jacksonIntegration = false
(since 2.3.7) to disable any out-of-the-box integration triggered @JsonSerialize
/@JsonDeserialize
, may help if integration is getting in the wayValue.Style.forceJacksonPropertyNames = false
to not use literal names in generated @JsonProperty
annotations. While somewhat fragile, an absence of the literal names enables the usage of naming strategies and built-in Jackson conventions.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());
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());
...
}
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 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?
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 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
com.google.gson.Gson
object with the @org.immutable.gson.Gson
umbrella annotation, but they are usually not used together in the same source file. If this will be huge PITA, please let us know!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:
META-INF/services/com.google.gson.TypeAdapterFactory
.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.
};
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 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
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.
Collection, optional and default attributes can be ignored during marshaling by using @Gson.Ignore
annotation.
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": [] }
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.
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
RuntimeException
s is thrown during deserializationWe 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: