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.

References