§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 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 following Jackson modules are enabled by default:
# The Jackson JSON serializer will register these modules.
# It is also possible to use jackson-modules = ["*"] to dynamically
# find and register all modules in the classpath.
jackson-modules = [
"com.fasterxml.jackson.module.paramnames.ParameterNamesModule",
"com.fasterxml.jackson.datatype.jdk8.Jdk8Module",
"com.fasterxml.jackson.datatype.jsr310.JavaTimeModule",
"com.fasterxml.jackson.datatype.pcollections.PCollectionsModule",
"com.fasterxml.jackson.datatype.guava.GuavaModule"]
You can amend the configuration lagom.serialization.json.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
.
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 1024 bytes. This threshold can be changed with configuration property lagom.serialization.json.compress-larger-than
.
§Schema Evolution
When working on long running projects using Persistence, or any kind of Event Sourcing, schema evolution becomes an important aspects 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 JacksonJsonMigration
.
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 com.lightbend.lagom.serialization.JacksonJsonMigration;
public class ItemAddedMigration extends JacksonJsonMigration {
@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 numer 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:
lagom.serialization.json.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 com.lightbend.lagom.serialization.JacksonJsonMigration;
public class ItemAddedMigration extends JacksonJsonMigration {
@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 arbitary 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 JacksonJsonMigration {
@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 JacksonJsonMigration {
@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.
lagom.serialization.json.migrations {
"com.myservice.event.OrderAdded" = "com.myservice.event.OrderPlacedMigration"
}