Serializer Extensions

SerializableAutoValueExtension can be extended to support additional un-serializable types. Extensions are created by implementing SerializerExtension.

Using SerializerExtensions

To use an extension that‘s not bundled with SerializableAutoValueExtension, include it in your AutoValue class’s dependencies.

Writing a SerializerExtension

To add serialization support to a new type, write a class that extends SerializerExtension. Put that class on the processorpath along with SerializerExtension.

See [AutoService] for Java extension loading information.

How SerializerExtension Works

SerializableAutoValueExtension iterates through each property of the @AutoValue class and tries to look up a SerializerExtension for the property's type. If a SerializerExtension is available, a Serializer is returned. The Serializer does three things:

  1. From the original property's type, determine what a suitable serializable type is.
  2. Generate an expression that maps the original property's value to the proxy value.
  3. Generate an expression that maps a proxy value back to the original property's value.

Example

We have a Foo AutoValue class that we would like to serialize:

@SerializableAutoValue
@AutoValue
public abstract class Foo implements Serializable {

  public static Foo create(Bar bar) {
    return new AutoValue_Foo(bar);
  }

  // Bar is unserializable.
  abstract Bar bar();
}

// An un-serializable class.
public final class Bar {

  public int x;

  public Bar(int x) {
    this.x = x;
  }
}

Unfortunately, Bar is un-serializable, which makes the entire class un-serializable. We can extend SerializableAutoValue to support Bar by implementing a SerializerExtension for Bar.

// Use AutoService to make BarSerializerExtension discoverable by java.util.ServiceLoader.
@AutoService(SerializerExtension.class)
public final class BarSerializerExtension implements SerializerExtension {

  // Service providers must have a public constructor with no formal parameters.
  public BarSerializerExtension() {}

  // This method is called for each property in an AutoValue.
  // When this extension can handle a type (i.e. Bar), we return a Serializer.
  called on all SerializerExtensions.
  @Override
  public Optional<Serializer> getSerializer(
      TypeMirror type,
      SerializerFactory factory,
      ProcessingEnvironment env) {
    // BarSerializerExtension only handles Bar types.
    if (!isBar(type)) {
      return Optional.empty();
    }

    return Optional.of(new BarSerializer(env));
  }

  // Our implementation of how Bar should be serialized + de-serialized.
  private static class BarSerializer implements Serializer {

    private final ProcessingEnvironment env;

    BarSerializer(ProcessingEnvironment env) {
      this.env = env;
    }

    // One way to serialize Bar is to just serialize Bar.x.
    // We can do that by mapping Bar to an int.
    @Override
    public TypeMirror proxyFieldType() {
      return Types.getPrimitiveType(TypeKind.INT);
    }

    // We need to map Bar to the type we declared in {@link #proxyFieldType}.
    // Note that {@code expression} is a variable of type Bar.
    @Override
    public CodeBlock toProxy(CodeBlock expression) {
      return CodeBlock.of("$L.x", expression);
    }

    // We need to map the integer back to a Bar.
    @Override
    public CodeBlock fromProxy(CodeBlock expression) {
      return CodeBlock.of("new $T($L)", Bar.class, expression);
    }
  }
}

BarSerializerExtension enables AutoValue classes with Bar properties to be serialized. For our example class Foo, it would help SerializableAutoValue generate the following code:

@Generated("SerializableAutoValueExtension")
final class AutoValue_Foo extends $AutoValue_Foo {

  Object writeReplace() throws ObjectStreamException {
    return new Proxy$(this.x);
  }

  static class Proxy$ implements Serializable {

    // The type is generated by {@code BarSerializer#proxyFieldType}.
    private int bar;

    Proxy$(Bar bar) {
      // The assignment expression is generated by {@code BarSerializer#toProxy}.
      this.bar = bar.x;
    }

    Object readResolve() throws ObjectStreamException {
      // The reverse mapping expression is generated by {@code BarSerializer#fromProxy}.
      return new AutoValue_Foo(new Bar(bar));
    }
  }
}

Type Parameters

Objects with type parameters are also supported by SerializerExtension.

For example:

// A potentially un-serializable class, depending on the actual type of T.
public final class Baz<T> implements Serializable {

  public T x;

  public Baz(int x) {
    this.x = x;
  }
}

Baz's type argument T may not be serializable, but we could create a SerializerExtension that supports Baz by asking for a SerializerExtension for T.

@AutoService(SerializerExtension.class)
public final class BazSerializerExtension implements SerializerExtension {

  public BazSerializerExtension() {}

  @Override
  public Optional<Serializer> getSerializer(
        TypeMirror type,
        SerializerFactory factory,
        ProcessingEnvironment env) {
    if (!isBaz(type)) {
      return Optional.empty();
    }

    // Extract the T of Baz<T>.
    TypeMirror containedType = getContainedType(type);

    // Look up a serializer for the contained type T.
    Serializer containedTypeSerializer = factory.getSerializer(containedType);

    // If the serializer for the contained type T is an identity function, it
    // means the contained type is either serializable or unsupported.
    // Either way, nothing needs to be done. Baz can be serialized as is.
    if (containedTypeSerializer.isIdentity()) {
      return Optional.empty();
    }

    // Make Baz serializable by using the contained type T serializer.
    return Optional.of(new BazSerializer(containedTypeSerializer));
  }

  private static class BazSerializer implements Serializer {

    private Serializer serializer;

    BazSerializer(Serializer serialize) {
      this.serializer = serializer;
    }

    @Override
    public TypeMirror proxyFieldType() {
      // Since the contained type "T" is Baz's only field, we map Baz to "T"'s
      // proxy type.
      return serializer.proxyFieldType();
    }

    @Override
    public CodeBlock toProxy(CodeBlock expression) {
      return serializer.toProxy(expression);
    }

    @Override
    public CodeBlock fromProxy(CodeBlock expression) {
      return serializer.fromProxy(expression);
    }
  }
}

This implementation uses SerializerFactory to find a Serializer for T. If a Serializer is available, we use it to map Baz to a serializable type. If no Serializer is available, we can do nothing and let Baz be serialized as-is.