virtuslab / akka-serialization-helper

Serialization toolbox for Akka messages, events and persistent state that helps achieve compile-time guarantee on serializability

Version Matrix

Akka Serialization Helper

Serialization toolbox for Akka messages, events and persistent state that helps achieve compile-time guarantee on serializability.

Install

To install the library use JitPack:

resolvers += "jitpack".at("https://jitpack.io")
val commit = "master-SNAPSHOT" //name of a branch with -SNAPSHOT or raw commit hash

Then, add one or more of the modules below:

Modules

The project consists of three modules that are independent of each other, comprising a complete solution together.

1. Check For Base Trait

A Scala compiler plugin that detects messages, events and persistent state, and checks whether they extend the base trait and report an error when they don't. This ensures that the specified serializer is used by Akka and protects against accidental use of Java serialization.

To use, just annotate a base trait with @org.virtuslab.ash.SerializabilityTrait:

@SerializabilityTrait
trait MySerializable

Installation:

libraryDependencies += "com.github.VirtusLab.akka-serialization-helper" %% "serializability-checker-library" % commit
libraryDependencies += compilerPlugin(
  "com.github.VirtusLab.akka-serialization-helper" %% "serializability-checker-compiler-plugin" % commit)

2. Dump Event Schema

An sbt plugin that allows for dumping schema of akka-persistence events to a file. Can be used for detecting accidental changes of events.

To dump events to <sbt-module>/target/<sbt-module-name>-dump-event-schema-<version>.json, run:

sbt dumpEventSchema

Installation:

Add to plugins.sbt:

libraryDependencies += "com.github.VirtusLab.akka-serialization-helper" % "sbt-dump-event-schema" % commit

To enable the plugin for a specific project, use:

enablePlugins(DumpEventSchemaPlugin)

You also have to change one setting responsible for resolving companion compiler plugin:

dumpEventSchema / dumpEventSchemaCompilerPlugin := "com.github.VirtusLab.akka-serialization-helper" % "dump-event-schema-compiler-plugin" % commit 

3. Serializer

Circe-based Akka serializer. It uses codecs, derived using Magnolia, that are generated during compile time (so serializer won't crash during runtime like reflection-based serializers may do).

libraryDependencies += "com.github.VirtusLab.akka-serialization-helper" %% "circe-akka-serializer" % commit

Comparison of available Akka Serializers

Serializer Jackson Circe Protobuf v3 Avro Borer Kryo
Data formats JSON or CBOR JSON JSON or custom binary JSON or custom binary JSON or CBOR custom binary
Scala support very poor, even with jackson-module-scala:
  • poor support for Scala objects, without configuration creates new instances of singleton types (Foo$), breaking pattern matching
  • lacks support of basic scala types like Unit
  • without explicit annotation doesn't work with generics extending AnyVal
perfect out of the box perfect with ScalaPB perfect with Avro4s perfect out of the box perfect out of the box
Akka support akka-serialization-jackson requires custom serializer used by akka-remote internally requires custom serializer requires custom serializer akka-kryo
Compile-time mechanics nothing happens in compile time; everything based on runtime reflection derives codecs via Magnolia with ScalaPB, generates Scala classes based on *.proto files with Avro4s, derives Avro schemas using Magnolia derives codecs without Magnolia with akka-kryo, optionally derives codecs in compile time, but otherwise uses reflection in runtime
Runtime safety none, uses reflection encoders and decoders are created during compilation *.proto files are validated before compilation Avro schema is created during compilation encoders and decoders are created during compilation depends on whether codecs were derived in compile time (then standard for Scala code), or not (than none)
Boilerplate a lot:
  • ADTs requires amount of annotation equal to or exceeding the actual type definitions
  • requires explicit serializers and deserializers in certain cases (e.g. enums)
every top-level sealed trait must be registered manually in case of custom types, a second layer of models is needed sometimes requires annotations every top-level sealed trait must be registered manually; every transitively included class must have an explicitly defined codec every top-level sealed trait must be registered manually
Schema evolution
  • removing field
  • adding optional field
with JacksonMigration:
  • adding mandatory field
  • renaming field
  • renaming class
  • support of forward versioning for rolling updates
  • adding optional field
  • removing optional field
  • adding required field with default value
  • removing required field
  • renaming field
  • reordering fields
  • transforming data before deserialization
  • adding optional field
  • removing optional field
  • adding required field with default value
  • removing required field
  • renaming field
  • reordering fields
  • changing between compatible types
  • reordering fields
  • renaming fields
  • adding optional field
  • adding required field with default value
  • removing field with default value
  • renaming fields
  • transforming data before deserialization
  • adding field
  • removing field
  • renaming field
  • renaming class