Skip to content

Latest commit

 

History

History
61 lines (47 loc) · 5.15 KB

serialization.md

File metadata and controls

61 lines (47 loc) · 5.15 KB

Serialization in CARP

This library supports serialization of CARP domain objects, for both the JVM and JavaScript runtime. This is achieved by relying on the kotlinx.serialization library and compiler plugin. Classes are made serializable by applying a @Serializable annotation to the class definition. Custom serializers for classes and properties can be specified through an optional KClass parameter of @Serializable which specifies the custom KSerializer<T> to use, e.g., @Serializable( CustomSerializer::class ).

Most of this is self-explanatory when looking at the codebase. StudyProtocolSnapshot is a good starting point to see how composite objects with complicated hierarchies of inheriting objects, including collections, can be serialized. But, what follows are pointers on how to use this library specific to CARP, which might be useful in case you need to understand the codebase, extend on base types, or introduce new ones.

UnknownPolymorphicSerializer: (De)serializing unknown types

A core design principle of our architecture is to be extensible. Many of the (domain model) classes contained in this library can be—and are meant to be—extended by external users. The question then arises how we should 'load' such custom classes when they are sent to our infrastructure which is unaware about them.

To enable this, we rely on a custom serializer for each of the base classes which are designed for extension. When encountering unknown types, these serializers extract the base properties they do know about, and 'wrap' the unknown type in a custom type definition deriving from the same base class, thereby providing transparent access to base properties without having to know the concrete type (the type does not need to be 'loaded' at runtime). When serializing unknown types, the original serialized form is output again. Thus, the 'wrapper' is only ever present while the custom domain objects are held in memory on a runtime which does not have the concrete type available. We envision this will greatly facilitate dealing with (or simply ignoring) such objects, making the codebase more stable and maintainable.

Serializing unknown types is currently only supported when using Json. Other formats, such as ProtoBuf or CBOR, will throw a SerializationException when trying to serialize types which are not registered for polymorphic serialization.

To facilitate the creation of these custom serializers, the abstract base class UnknownPolymorphicSerializer<P: Any, W: P> can be used (suggestions for a better name are still welcome). This is a "serializer for polymorph objects of type [P] which wraps extending types unknown at runtime as instances of type [W]." A helper function createUnknownPolymorphicSerializer simplifies creating these concrete classes by only having to pass a method which constructs the custom wrapper based on incoming JSON data. For example, the following code creates an UnknownPolymorphicSerializer for an abstract TaskConfiguration class.

object TaskConfigurationSerializer : KSerializer<TaskConfiguration>
    by createUnknownPolymorphicSerializer( { className, json, serializer -> CustomTaskConfiguration( className, json, serializer ) } )

The custom wrapper needs to:

  1. Extend from the base class. E.g., TaskConfiguration.
  2. Implement UnknownPolymorphicWrapper, which simply provides access to the class name and original JSON source.
  3. Apply @Serializable to use the custom serializer. E.g, @Serializable( TaskConfigurationSerializer::class ).

The only real work lies in extracting the base properties using traditional JSON parsing. This can easily be done using a serializer of an intermediate concrete type, as exemplified in CustomTaskConfiguration.

The custom serializer should be configured as the default serializer for the expected base type in the SerializersModule, and the wrapper should be registered as a subclass. For example, TaskConfigurationSerializer is the UnknownPolymorphicSerializer for the TaskConfiguration base class, which is registered in the common subsystem SerializersModule:

polymorphic( TaskConfiguration::class )
{
    subclass( ConcurrentTask::class )
    ...
    subclass( CustomTaskConfiguration::class )
    default { TaskConfigurationSerializer }
}

JavaScript compiler plugin limitations

The serializer for classes with named companion objects cannot be found. We recommend not to name companion objects. The compiler plugin adds a serializer() method to the companion object, and the JavaScript runtime relies on the default name of companion objects (Companion). This is something I fixed for the JVM runtime, but seems harder to do for the JS runtime.