It has been about 1 month now since I started writing REST APIs for Nuxeo. We use Google AutoValue and Jackson for data transfer objects (DTO). Today, I would like share some feedback with you.
What is AutoValue?
Value classes are extremely common in Java projects. These are classes for which you want to treat any two instances with suitably equal field values as interchangeable. AutoValue provides an easier way to create immutable value classes, with a lot less code and less room for error, while not restricting your freedom to code almost any aspect of your class exactly the way you want it.
The class using AutoValue and Jackson looks like:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.auto.value.AutoValue;
@AutoValue
@JsonDeserialize(builder = AutoValue_Company.Builder.class)
public abstract class Company {
public static Builder newBuilder() {
return new AutoValue_Company.Builder();
}
@JsonProperty("id")
public abstract long id();
@JsonProperty("description")
public abstract String description();
@JsonProperty("websiteUrl")
public abstract String websiteUrl();
@AutoValue.Builder
public interface Builder {
@JsonProperty("id")
Builder id(long id);
@JsonProperty("description")
Builder description(String description);
@JsonProperty("websiteUrl")
Builder websiteUrl(String url);
Company build();
}
}
Note that we only define the abstract classes — the concrete classes are generated by AutoValue using Annotation Processor:
AutoValue_Company.class
AutoValue_Company.Builder.class
The naming convention for AutoValue is having prefix “AutoValue_“, and continue with the class name of the abstract class:
AutoValue_<ClassName>.class
If you want to know more about annotation processor, take a look at OpenJDK: Compilation Overview.
Jackson
Jackson annotations help Jackson to understand how to serialize and deserialize the value class.
Action | Description |
---|---|
Serialization | Java → JSON using value class annotations |
Deserialization | JSON → Java using builder class annotations |
When using Jackson in JAX-RS, a Jackson JSON provider needs to be registered as singleton in the REST application:
import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
@ApplicationPath("api")
public class RestApplication extends Application {
...
@Override
public Set<Object> getSingletons() {
Set<Object> singletons = new HashSet<>();
singletons.add(new JacksonJsonProvider());
return singletons;
}
}
You might need a more complex Jackson JSON provider to fit your business logic, see section Jackson Advanced Configuration.
AutoValue Advanced Configuration
In the following sections, we’ll talk about advanced configuration: ensure the solution “AutoValue + Jackson” fits your application requirements.
Optional Value
Some fields might be optional in your AutoValue object. Ordinarily the
generated constructor will reject any null
values. If you want to accept
null
, you can either add a @Nullable
annotation (see AutoValue: how to
use nullable properties?) or use Optional<T>
. My preferred one is
Optional<T>
:
@AutoValue
@JsonDeserialize(builder = AutoValue_User.Builder.class)
public abstract class User {
/**
* User description.
*
* <p>Optional because user might not want to provide any information.
*/
@JsonProperty("description")
public abstract Optional<String> description(); // 1
...
@AutoValue.Builder
public interface Builder {
@JsonProperty("description")
Builder description(Optional<String> description); // 2
Builder description(String description); // 3
...
}
}
Note that on the getter side (1), there is only one getter method, which return
optional. On the other side, for setter, there are two setter methods (2)(3),
allowing you to provide a description directly or a wrapped description. For
Jackson, you must annotate the builder method with Optional<T>
as parameter,
otherwise AutoValue reject the null
case provided by Jackson.
Jackson Advanced Configuration
This section describes how to customize your Jackson JSON provider. Here’s a template for bootstrapping the customization, but of course you can do it in other ways too:
@ApplicationPath("api")
public class RestApplication extends Application {
...
@Override
public Set<Object> getSingletons() {
Set<Object> set = new HashSet<>();
set.add(new JacksonJsonProvider(newObjectMapper()));
return set;
}
private static ObjectMapper newObjectMapper() {
ObjectMapper m = new ObjectMapper();
// Customization goes here...
return m;
}
}
Enable Java 8 Support
Register the following modules into your ObjectMapper
to enable to Java 8
supports, including Java Time:
ObjectMapper mapper = new ObjectMapper();
.registerModule(new ParameterNamesModule())
.registerModule(new Jdk8Module())
// new module, NOT JSR310Module
.registerModule(new JavaTimeModule());
These modules require the following dependencies:
com.fasterxml.jackson.module:jackson-module-parameter-names
com.fasterxml.jackson.datatype:jackson-datatype-jdk8
com.fasterxml.jackson.datatype:jackson-datatype-jsr310
See GitHub project FasterXML/jackson-modules-java8 for more configuration detail.
Enable ISO-8601 For DateTime Serialization
If you want the datetime fields to be serialized as ISO-8601, you need to
explicitly set the date format as StdDateFormat
for standard serializers and
deserializers. Therefore, for serialization it defaults to using an ISO-8601
compliant format (format String yyyy-MM-dd'T'HH:mm:ss.SSSZ
)
and for deserialization, both ISO-8601 and RFC-1123. You also need to disable
the serialization feature WRITE_DATES_AS_TIMESTAMPS, which serializes the date
time to timestamp. The final code block:
mapper.setDateFormat(new StdDateFormat());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
Preserve JSON Timezone After Deserialization
By default, mapper drops timezone during deserialization. It adjust dates to context’s (web application’s) timezone. If you want to preserve user’s timezone, you can do the following:
mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
This is useful when you’ve users coming from different timezones, e.g. from France and China, and you don’t want to modify the datetime created by users. However, Jackson is not the only part to take care — you need to ensure the entire stack of your application supports timezone, including database.