Serialization with Jackson

Dependency

To use Jackson Serialization, you must add the following dependency in your project:

sbt
libraryDependencies += "com.typesafe.akka" %% "akka-serialization-jackson" % "2.6-SNAPSHOT"
Maven
<dependency>
  <groupId>com.typesafe.akka</groupId>
  <artifactId>akka-serialization-jackson_2.12</artifactId>
  <version>2.6-SNAPSHOT</version>
</dependency>
Gradle
dependencies {
  compile group: 'com.typesafe.akka', name: 'akka-serialization-jackson_2.12', version: '2.6-SNAPSHOT'
}

Introduction

You find general concepts for for Akka serialization in the Serialization section. This section describes how to use the Jackson serializer for application specific messages and persistent events and snapshots.

Jackson has support for both text based JSON and binary formats.

In many cases ordinary classes can be serialized by Jackson without any additional hints, but sometimes annotations are needed to specify how to convert the objects to JSON/bytes.

Usage

To enable Jackson serialization for a class you need to configure it or one of its super classes in serialization-bindings configuration. Typically you will create a marker traitinterface for that purpose and let the messages extendimplement that.

Scala
/**
 * Marker interface for messages, events and snapshots that are serialized with Jackson.
 */
trait MySerializable

final case class Message(name: String, nr: Int) extends MySerializable
Java
/** Marker interface for messages, events and snapshots that are serialized with Jackson. */
public interface MySerializable {}

class MyMessage implements MySerializable {
  public final String name;
  public final int nr;

  public MyMessage(String name, int nr) {
    this.name = name;
    this.nr = nr;
  }
}

Then you configure the class name of the marker traitinterface in serialization-bindings to one of the supported Jackson formats: jackson-json or jackson-cbor

akka.actor {
  serialization-bindings {
    "com.myservice.MySerializable" = jackson-json
  }
}

A good convention would be to name the marker interface CborSerializable or JsonSerializable. In this documentation we have used MySerializable to make it clear that the marker interface itself is not provided by Akka.

That is all that is needed for basic classes where Jackson understands the structure. A few cases that requires annotations are described below.

Note that it’s only the top level class or its marker traitinterface that must be defined in serialization-bindings, not nested classes that it references in member fields.

Note

Add the -parameters Java compiler option for usage by the ParameterNamesModule. It reduces the need for some annotations.

Security

For security reasons it is disallowed to bind the Jackson serializers to open ended types that might be a target for serialization gadgets, such as:

  • java.lang.Object
  • java.io.Serializable
  • java.util.Comparable.

The blacklist of possible serialization gadget classes defined by Jackson databind are checked and disallowed for deserialization.

Warning

Don’t use @JsonTypeInfo(use = Id.CLASS) or ObjectMapper.enableDefaultTyping since that is a security risk when using polymorphic types.

Formats

The following formats are supported, and you select which one to use in the serialization-bindings configuration as described above.

  • jackson-json - ordinary text based JSON
  • jackson-cbor - binary CBOR data format

The binary format is more compact, with slightly better performance than the JSON format.

Annotations

Constructor with single parameter

You might run into an exception like this:

MismatchedInputException: Cannot construct instance of `...` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)

That is probably because the class has a constructor with a single parameter, like:

Java
public class SimpleCommand implements MySerializable {
  private final String name;

  public SimpleCommand(String name) {
    this.name = name;
  }
}

That can be solved by adding @JsonCreator or @JsonProperty annotations:

Java
public class SimpleCommand implements MySerializable {
  private final String name;

  @JsonCreator
  public SimpleCommand(String name) {
    this.name = name;
  }
}

or

Java
public class SimpleCommand implements MySerializable {
  private final String name;

  public SimpleCommand(@JsonProperty("name") String name) {
    this.name = name;
  }
}

The ParameterNamesModule is configured with JsonCreator.Mode.PROPERTIES as described in the Jackson documentation

Polymorphic types

A polymorphic type is when a certain base type has multiple alternative implementations. When nested fields or collections are of polymorphic type the concrete implementations of the type must be listed with @JsonTypeInfo and @JsonSubTypes annotations.

Example:

Scala
final case class Zoo(primaryAttraction: Animal) extends MySerializable

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes(
  Array(
    new JsonSubTypes.Type(value = classOf[Lion], name = "lion"),
    new JsonSubTypes.Type(value = classOf[Elephant], name = "elephant")))
sealed trait Animal

final case class Lion(name: String) extends Animal

final case class Elephant(name: String, age: Int) extends Animal
Java
public class Zoo implements MySerializable {
  public final Animal primaryAttraction;

  @JsonCreator
  public Zoo(Animal primaryAttraction) {
    this.primaryAttraction = primaryAttraction;
  }
}

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
  @JsonSubTypes.Type(value = Lion.class, name = "lion"),
  @JsonSubTypes.Type(value = Elephant.class, name = "elephant")
})
interface Animal {}

public final class Lion implements Animal {
  public final String name;

  @JsonCreator
  public Lion(String name) {
    this.name = name;
  }
}

public final class Elephant implements Animal {
  public final String name;
  public final int age;

  public Elephant(String name, int age) {
    this.name = name;
    this.age = age;
  }
}

If you haven’t defined the annotations you will see an exception like this:

InvalidDefinitionException: Cannot construct instance of `...` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information

Note that this is not needed for a top level class, but for fields inside it. In this example Animal is used inside of Zoo, which is sent as a message or persisted. If Animal was sent or persisted standalone the annotations are not needed because then it is the concrete subclasses Lion or Elephant that are serialized.

When specifying allowed subclasses with those annotations the class names will not be included in the serialized representation and that is important for preventing loading of malicious serialization gadgets when deserializing.

Warning

Don’t use @JsonTypeInfo(use = Id.CLASS) or ObjectMapper.enableDefaultTyping since that is a security risk when using polymorphic types.

ADT with trait and case object

In Scala it’s common to use a sealed trait and case objects to represent enums. If the values are case classes the @JsonSubTypes annotation as described above works, but if the values are case objects it will not. The annotation requires a Class and there is no way to define that in an annotation for a case object.

This can be solved by implementing a custom serialization for the enums. Annotate the trait with @JsonSerialize and @JsonDeserialize and implement the serialization with StdSerializer and StdDeserializer.

Scala
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.ser.std.StdSerializer

@JsonSerialize(using = classOf[DirectionJsonSerializer])
@JsonDeserialize(using = classOf[DirectionJsonDeserializer])
sealed trait Direction

object Direction {
  case object North extends Direction
  case object East extends Direction
  case object South extends Direction
  case object West extends Direction
}

class DirectionJsonSerializer extends StdSerializer[Direction](classOf[Direction]) {
  import Direction._

  override def serialize(value: Direction, gen: JsonGenerator, provider: SerializerProvider): Unit = {
    val strValue = value match {
      case North => "N"
      case East  => "E"
      case South => "S"
      case West  => "W"
    }
    gen.writeString(strValue)
  }
}

class DirectionJsonDeserializer extends StdDeserializer[Direction](classOf[Direction]) {
  import Direction._

  override def deserialize(p: JsonParser, ctxt: DeserializationContext): Direction = {
    p.getText match {
      case "N" => North
      case "E" => East
      case "S" => South
      case "W" => West
    }
  }
}

final case class Compass(currentDirection: Direction) extends MySerializable

Schema Evolution

When using Event Sourcing, but also for rolling updates, 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.

The Jackson serializer provides a way to perform transformations of the JSON tree model during deserialization. This is working in the same way for the textual and binary formats.

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 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 NoneOptional.empty.

Old class:

Scala
case class ItemAdded(shoppingCartId: String, productId: String, quantity: Int) extends MySerializable
Java
public class ItemAdded implements MySerializable {
  public final String shoppingCartId;
  public final String productId;
  public final int quantity;

  public ItemAdded(String shoppingCartId, String productId, int quantity) {
    this.shoppingCartId = shoppingCartId;
    this.productId = productId;
    this.quantity = quantity;
  }
}

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

Scala
case class ItemAdded(shoppingCartId: String, productId: String, quantity: Int, discount: Option[Double], note: String)
    extends MySerializable {

  // alternative constructor because `note` should have default value "" when not defined in json
  @JsonCreator
  def this(shoppingCartId: String, productId: String, quantity: Int, discount: Option[Double], note: Option[String]) =
    this(shoppingCartId, productId, quantity, discount, note.getOrElse(""))
}
Java
public class ItemAdded implements MySerializable {
  public final String shoppingCartId;
  public final String productId;
  public final int quantity;
  public final Optional<Double> discount;
  public final String note;

  @JsonCreator
  public ItemAdded(
      String shoppingCartId,
      String productId,
      int quantity,
      Optional<Double> discount,
      String note) {
    this.shoppingCartId = shoppingCartId;
    this.productId = productId;
    this.quantity = quantity;
    this.discount = discount;

    // default for note is "" if not included in json
    if (note == null) this.note = "";
    else this.note = note;
  }

  public ItemAdded(
      String shoppingCartId, String productId, int quantity, Optional<Double> discount) {
    this(shoppingCartId, productId, quantity, discount, "");
  }
}

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

Scala
case class ItemAdded(shoppingCartId: String, productId: String, quantity: Int, discount: Double) extends MySerializable
Java
public class ItemAdded implements MySerializable {
  public final String shoppingCartId;
  public final String productId;
  public final int quantity;
  public final double discount;

  public ItemAdded(String shoppingCartId, String productId, int quantity, double discount) {
    this.shoppingCartId = shoppingCartId;
    this.productId = productId;
    this.quantity = quantity;
    this.discount = discount;
  }
}

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

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

Scala
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

class ItemAddedMigration extends JacksonMigration {

  override def currentVersion: Int = 2

  override def transform(fromVersion: Int, json: JsonNode): JsonNode = {
    val root = json.asInstanceOf[ObjectNode]
    if (fromVersion <= 1) {
      root.set("discount", DoubleNode.valueOf(0.0))
    }
    root
  }
}
Java
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"
}

The same thing could have been done for the note field, adding a default value of "" in the ItemAddedMigration.

Rename Field

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

Scala
case class ItemAdded(shoppingCartId: String, itemId: String, quantity: Int) extends MySerializable
Java
public class ItemAdded implements MySerializable {
  public final String shoppingCartId;

  public final String itemId;

  public final int quantity;

  public ItemAdded(String shoppingCartId, String itemId, int quantity) {
    this.shoppingCartId = shoppingCartId;
    this.itemId = itemId;
    this.quantity = quantity;
  }
}

The migration code would look like:

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

class ItemAddedMigration extends JacksonMigration {

  override def currentVersion: Int = 2

  override def transform(fromVersion: Int, json: JsonNode): JsonNode = {
    val root = json.asInstanceOf[ObjectNode]
    if (fromVersion <= 1) {
      root.set("itemId", root.get("productId"))
      root.remove("productId")
    }
    root
  }
}
Java

import akka.serialization.jackson.JacksonMigration; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; 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:

Scala
case class Customer(name: String, street: String, city: String, zipCode: String, country: String) extends MySerializable
Java
public class Customer implements MySerializable {
  public final String name;
  public final String street;
  public final String city;
  public final String zipCode;
  public final String country;

  public Customer(String name, String street, String city, String zipCode, String country) {
    this.name = name;
    this.street = street;
    this.city = city;
    this.zipCode = zipCode;
    this.country = country;
  }
}

New class:

Scala
case class Customer(name: String, shippingAddress: Address, billingAddress: Option[Address]) extends MySerializable
Java
public class Customer implements MySerializable {
  public final String name;
  public final Address shippingAddress;
  public final Optional<Address> billingAddress;

  public Customer(String name, Address shippingAddress, Optional<Address> billingAddress) {
    this.name = name;
    this.shippingAddress = shippingAddress;
    this.billingAddress = billingAddress;
  }
}

with the Address class:

Scala
case class Address(street: String, city: String, zipCode: String, country: String) extends MySerializable
Java
public class Address {
  public final String street;
  public final String city;
  public final String zipCode;
  public final String country;

  public Address(String street, String city, String zipCode, String country) {
    this.street = street;
    this.city = city;
    this.zipCode = zipCode;
    this.country = country;
  }
}

The migration code would look like:

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

class CustomerMigration extends JacksonMigration {

  override def currentVersion: Int = 2

  override def transform(fromVersion: Int, json: JsonNode): JsonNode = {
    val root = json.asInstanceOf[ObjectNode]
    if (fromVersion <= 1) {
      val 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")
    }
    root
  }
}
Java
import akka.serialization.jackson.JacksonMigration;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

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:

Scala
case class OrderAdded(shoppingCartId: String) extends MySerializable
Java
public class OrderAdded implements MySerializable {
  public final String shoppingCartId;

  @JsonCreator
  public OrderAdded(String shoppingCartId) {
    this.shoppingCartId = shoppingCartId;
  }
}

New class:

Scala
case class OrderPlaced(shoppingCartId: String) extends MySerializable
Java
public class OrderPlaced implements MySerializable {
  public final String shoppingCartId;

  @JsonCreator
  public OrderPlaced(String shoppingCartId) {
    this.shoppingCartId = shoppingCartId;
  }
}

The migration code would look like:

Scala
class OrderPlacedMigration extends JacksonMigration {

  override def currentVersion: Int = 2

  override def transformClassName(fromVersion: Int, className: String): String =
    classOf[OrderPlaced].getName

  override def transform(fromVersion: Int, json: JsonNode): JsonNode = json
}
Java
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"
}

Remove from serialization-bindings

When a class is not used for serialization any more it can be removed from serialization-bindings but to still allow deserialization it must then be listed in the whitelist-class-prefix configuration. This is useful for example during rolling update with serialization changes, or when reading old stored data. It can also be used when changing from Jackson serializer to another serializer (e.g. Protobuf) and thereby changing the serialization binding, but it should still be possible to deserialize old data with Jackson.

akka.serialization.jackson.whitelist-class-prefix = 
  ["com.myservice.event.OrderAdded", "com.myservice.command"]

It’s a list of class names or prefixes of class names.

Jackson Modules

The following Jackson modules are enabled by default:

akka.serialization.jackson {

  # The Jackson JSON serializer will register these modules.
  jackson-modules += "akka.serialization.jackson.AkkaJacksonModule"
  # AkkaTypedJacksonModule optionally included if akka-actor-typed is in classpath
  jackson-modules += "akka.serialization.jackson.AkkaTypedJacksonModule"
  // FIXME how does that optinal loading work??
  # AkkaStreamsModule optionally included if akka-streams is in classpath
  jackson-modules += "akka.serialization.jackson.AkkaStreamJacksonModule"
  jackson-modules += "com.fasterxml.jackson.module.paramnames.ParameterNamesModule"
  jackson-modules += "com.fasterxml.jackson.datatype.jdk8.Jdk8Module"
  jackson-modules += "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"
  jackson-modules += "com.fasterxml.jackson.module.scala.DefaultScalaModule"
}

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.

Compression

JSON can be rather verbose and for large messages it can be beneficial compress large payloads. Messages larger than the following configuration are compressed with GZIP.

akka.serialization.jackson {
  # The serializer will compress the payload when it's larger than this value.
  # Compression can be disabled with value 'off'.
  compress-larger-than = 32 KiB
}

Compression can be disabled by setting this configuration property to off. It will still be able to decompress payloads that were compressed when serialized, e.g. if this configuration is changed.

Additional configuration

Configuration per binding

By default the configuration for the Jackson serializers and their ObjectMappers is defined in the akka.serialization.jackson section. It is possible to override that configuration in a more specific akka.serialization.jackson.<binding name> section.

akka.serialization.jackson.jackson-json {
  serialization-features {
    WRITE_DATES_AS_TIMESTAMPS = off
  }
}
akka.serialization.jackson.jackson-cbor {
  serialization-features {
    WRITE_DATES_AS_TIMESTAMPS = on
  }
}

It’s also possible to define several bindings and use different configuration for them. For example, different settings for remote messages and persisted events.

akka.actor {
  serializers {
    jackson-json-message = "akka.serialization.jackson.JacksonJsonSerializer"
    jackson-json-event   = "akka.serialization.jackson.JacksonJsonSerializer"
  }
  serialization-identifiers {
    jackson-json-message = 9001
    jackson-json-event = 9002
  }
  serialization-bindings {
    "com.myservice.MyMessage" = jackson-json-message
    "com.myservice.MyEvent" = jackson-json-event
  }
}
akka.serialization.jackson {
  jackson-json-message {
    serialization-features {
      WRITE_DATES_AS_TIMESTAMPS = on
    }
  }
  jackson-json-event {
    serialization-features {
      WRITE_DATES_AS_TIMESTAMPS = off
    }
  }
}

Additional features

Additional Jackson serialization features can be enabled/disabled in configuration. The default values from Jackson are used aside from the the following that are changed in Akka’s default configuration.

akka.serialization.jackson {
  # Configuration of the ObjectMapper serialization features.
  # See com.fasterxml.jackson.databind.SerializationFeature
  # Enum values corresponding to the SerializationFeature and their boolean value.
  serialization-features {
    # Date/time in ISO-8601 (rfc3339) yyyy-MM-dd'T'HH:mm:ss.SSSZ format
    # as defined by com.fasterxml.jackson.databind.util.StdDateFormat
    # For interoperability it's better to use the ISO format, i.e. WRITE_DATES_AS_TIMESTAMPS=off,
    # but WRITE_DATES_AS_TIMESTAMPS=on has better performance.
    WRITE_DATES_AS_TIMESTAMPS = off
  }

  # Configuration of the ObjectMapper deserialization features.
  # See com.fasterxml.jackson.databind.DeserializationFeature
  # Enum values corresponding to the DeserializationFeature and their boolean value.
  deserialization-features {
    FAIL_ON_UNKNOWN_PROPERTIES = off
  }

  # Configuration of the ObjectMapper mapper features.
  # See com.fasterxml.jackson.databind.MapperFeature
  # Enum values corresponding to the MapperFeature and their
  # boolean values, for example:
  #
  # mapper-features {
  #   SORT_PROPERTIES_ALPHABETICALLY = on
  # }
  mapper-features {}

  # Configuration of the ObjectMapper JsonParser features.
  # See com.fasterxml.jackson.core.JsonParser.Feature
  # Enum values corresponding to the JsonParser.Feature and their
  # boolean value, for example:
  #
  # json-parser-features {
  #   ALLOW_SINGLE_QUOTES = on
  # }
  json-parser-features {}

  # Configuration of the ObjectMapper JsonParser features.
  # See com.fasterxml.jackson.core.JsonGenerator.Feature
  # Enum values corresponding to the JsonGenerator.Feature and
  # their boolean value, for example:
  #
  # json-generator-features {
  #   WRITE_NUMBERS_AS_STRINGS = on
  # }
  json-generator-features {}

  # Additional classes that are allowed even if they are not defined in `serialization-bindings`.
  # This is useful when a class is not used for serialization any more and therefore removed
  # from `serialization-bindings`, but should still be possible to deserialize.
  whitelist-class-prefix = []

  # Specific settings for jackson-json binding can be defined in this section to
  # override the settings in 'akka.serialization.jackson'
  jackson-json {}

  # Specific settings for jackson-cbor binding can be defined in this section to
  # override the settings in 'akka.serialization.jackson'
  jackson-cbor {}

}

Date/time format

WRITE_DATES_AS_TIMESTAMPS is by default disabled, which means that date/time fields are serialized in ISO-8601 (rfc3339) yyyy-MM-dd'T'HH:mm:ss.SSSZ format instead of numeric arrays. This is better for interoperability but it is slower. If you don’t need the ISO format for interoperability with external systems you can change the following configuration for better performance of date/time fields.

akka.serialization.jackson.serialization-features {
  WRITE_DATES_AS_TIMESTAMPS = on
}

Jackson is still be able to deserialize the other format independent of this setting.

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.