Serialization

§Serialization

Out of the box, Lagom will use JSON for request and response message format for the external API of the service, using Jackson to serialize and deserialize messages. The messages that are sent within the cluster of the service must also be serializable and so must the events that are stored by Akka Persistence Typed Behaviors and Lagom Persistent Entities. We recommend JSON for these as well and Lagom makes it easy to add Jackson serialization support to such classes.

Do not depend on Java serialization for production deployments. It is inefficient both in serialization size and speed. It is very difficult to evolve the classes when using Java serialization, which is especially important for the persistent state and events, since you must be able to deserialize old objects that were stored.

§Enabling JSON Serialization

To enable JSON serialization for a class you need to implement the Jsonable marker interface.

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.lightbend.lagom.javadsl.immutable.ImmutableStyle;
import com.lightbend.lagom.serialization.Jsonable;
import org.immutables.value.Value;

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = User.class)
public interface AbstractUser extends Jsonable {

  String getName();

  String getEmail();
}

Note that we’re using the Immutables library here, so this will generate an immutable User class. This is the reason for adding the @JsonDeserialize annotation.

§Jackson Modules

The enabled Jackson modules are listed in the Akka documentation, and additionally following Jackson modules are enabled by default:

# The Jackson JSON serializer will register these modules, and
# the ones defined in reference.conf of akka-serialization-jackson.
akka.serialization.jackson {
  jackson-modules += "com.fasterxml.jackson.datatype.pcollections.PCollectionsModule"
  jackson-modules += "com.fasterxml.jackson.datatype.guava.GuavaModule"
}

You can amend the configuration akka.serialization.jackson.jackson-modules to enable other modules.

The ParameterNamesModule requires that the -parameters Java compiler option is enabled.

The section Immutable Objects contains more examples of classes that are Jsonable.

If you use Lagom Persistence (classic) for your persistence, you can use the PersistentEntityTestDriver that is described in the Persistent Entity Unit Testing section to verify that all commands, events, replies and state are serializable.

§Compression

Compression, as described here, is only used for persistent events, persistent snapshots and remote messages with the service cluster. It is not used for messages that are serialized in the external API of the service.

JSON can be rather verbose and for large messages it can be beneficial to enable compression. That is done by using the CompressedJsonable instead of the Jsonable marker interface.

import com.lightbend.lagom.serialization.CompressedJsonable;

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = Author.class)
public interface AbstractAuthor extends CompressedJsonable {

  String getName();

  String biography();
}

The serializer will by default only compress messages that are larger than 32 KiB. This threshold can be changed with configuration property akka.serialization.jackson.jackson-json-compressed.compression.compress-larger-than.

§Schema Evolution

When working on long running projects using Akka Persistence Typed, Lagom Persistence (classic), or any kind of Event Sourcing, schema evolution becomes an important aspect of developing your application. The requirements as well as our own understanding of the business domain may (and will) change over time.

Lagom provides a way to perform transformations of the JSON tree model during deserialization.

We will look at a few scenarios of how the classes may be evolved.

§Remove Field

Removing a field can be done without any migration code. The Jackson JSON serializer will ignore properties that does not exist in the class.

§Add Field

Adding an optional field can be done without any migration code. The default value will be Optional.empty.

Old class:

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = ItemAdded.class)
public interface AbstractItemAdded extends Jsonable {

  String getShoppingCartId();

  String getProductId();

  int getQuantity();
}

New class with a new optional discount property and a new note field with default value:

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = ItemAdded.class)
public interface AbstractItemAdded extends Jsonable {

  String getShoppingCartId();

  String getProductId();

  int getQuantity();

  Optional<Double> getDiscount();

  @Value.Default
  default String getNote() {
    return "";
  }
}

Let’s say we want to have a mandatory discount property without default value instead:

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = ItemAdded.class)
public interface AbstractItemAdded extends Jsonable {

  String getShoppingCartId();

  String getProductId();

  int getQuantity();

  double getDiscount();
}

To add a new mandatory field we have to use a JSON migration class and set the default value in the migration code, which extends the JacksonMigration.

This is how a migration class would look like for adding a discount field:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import akka.serialization.jackson.JacksonMigration;

public class ItemAddedMigration extends JacksonMigration {

  @Override
  public int currentVersion() {
    return 2;
  }

  @Override
  public JsonNode transform(int fromVersion, JsonNode json) {
    ObjectNode root = (ObjectNode) json;
    if (fromVersion <= 1) {
      root.set("discount", DoubleNode.valueOf(0.0));
    }
    return root;
  }
}

Override the currentVersion method to define the version number of the current (latest) version. The first version, when no migration was used, is always 1. Increase this version number whenever you perform a change that is not backwards compatible without migration code.

Implement the transformation of the old JSON structure to the new JSON structure in the transform method. The JsonNode is mutable so you can add and remove fields, or change values. Note that you have to cast to specific sub-classes such as ObjectNode and ArrayNode to get access to mutators.

The migration class must be defined in configuration file:

akka.serialization.jackson.migrations {
  "com.myservice.event.ItemAdded" = "com.myservice.event.ItemAddedMigration"
}

§Rename Field

Let’s say that we want to rename the productId field to itemId in the previous example.

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = ItemAdded.class)
public interface AbstractItemAdded extends Jsonable {

  String getShoppingCartId();

  String getItemId();

  int getQuantity();
}

The migration code would look like:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import akka.serialization.jackson.JacksonMigration;

public class ItemAddedMigration extends JacksonMigration {

  @Override
  public int currentVersion() {
    return 2;
  }

  @Override
  public JsonNode transform(int fromVersion, JsonNode json) {
    ObjectNode root = (ObjectNode) json;
    if (fromVersion <= 1) {
      root.set("itemId", root.get("productId"));
      root.remove("productId");
    }
    return root;
  }
}

§Structural Changes

In a similar way we can do arbitrary structural changes.

Old class:

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = Customer.class)
public interface AbstractCustomer extends Jsonable {
  String getName();

  String getStreet();

  String getCity();

  String getZipCode();

  String getCountry();
}

New class:

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = Customer.class)
public interface AbstractCustomer extends Jsonable {
  String getName();

  Address getShippingAddress();

  Optional<Address> getBillingAddress();
}

with the Address class:

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = Address.class)
public interface AbstractAddress extends Jsonable {
  String getStreet();

  String getCity();

  String getZipCode();

  String getCountry();
}

The migration code would look like:

public class CustomerMigration extends JacksonMigration {

  @Override
  public int currentVersion() {
    return 2;
  }

  @Override
  public JsonNode transform(int fromVersion, JsonNode json) {
    ObjectNode root = (ObjectNode) json;
    if (fromVersion <= 1) {
      ObjectNode shippingAddress = root.with("shippingAddress");
      shippingAddress.set("street", root.get("street"));
      shippingAddress.set("city", root.get("city"));
      shippingAddress.set("zipCode", root.get("zipCode"));
      shippingAddress.set("country", root.get("country"));
      root.remove("street");
      root.remove("city");
      root.remove("zipCode");
      root.remove("country");
    }
    return root;
  }
}

§Rename Class

It is also possible to rename the class. For example, let’s rename OrderAdded to OrderPlaced.

Old class:

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = OrderAdded.class)
public interface AbstractOrderAdded extends Jsonable {
  String getShoppingCartId();
}

New class:

@Value.Immutable
@ImmutableStyle
@JsonDeserialize(as = OrderPlaced.class)
public interface AbstractOrderPlaced extends Jsonable {
  String getShoppingCartId();
}

The migration code would look like:


public class OrderPlacedMigration extends JacksonMigration { @Override public int currentVersion() { return 2; } @Override public String transformClassName(int fromVersion, String className) { return OrderPlaced.class.getName(); } @Override public JsonNode transform(int fromVersion, JsonNode json) { return json; } }

Note the override of the transformClassName method to define the new class name.

That type of migration must be configured with the old class name as key. The actual class can be removed.

akka.serialization.jackson.migrations {
  "com.myservice.event.OrderAdded" = "com.myservice.event.OrderPlacedMigration"
}

Found an error in this documentation? The source code for this page can be found here. Please feel free to edit and contribute a pull request.