💾 Serialization
Serializing and deserializing logic blocks.
LogicBlocks can be serialized by using the Chickensoft Introspection and Serialization packages together.
Like LogicBlocks, the Serialization package builds off the Introspection package to provide a simple way of serializing and deserializing polymorphic objects, as well as support for versioning.
The serialization system is able to be used alongside System.Text.Json, since it uses the metadata generated by the Introspection generator to describe the types and information needed System.Text.Json to serialize an object — it's essentially an alternative (but compatible) generator for System.Text.Json that can be mixed and matched with other System.Text.Json generated contexts.
Using generated metadata allows the serialization system to work for ahead-of-time (AOT) platforms like iOS.
💽 Defining a Serializable Logic Block
To make a logic block serializable, it needs to be an introspective and identifiable type, as well as have a parameterless constructor.
We've been using the [Meta]
attribute throughout the documentation to make our logic blocks introspective. LogicBlocks uses the generated metadata to automatically preallocate states when our logic block is constructed (if it's an introspective type).
Now, we simply need to add the [Id]
attribute to the logic block itself, as well as make each state introspective. Additionally, non-abstract states need to be given an [Id]
, too.
Serializable type [Id]
's need to be globally unique across all serializable types used in your project.
The [Id]
attribute is used as the type discriminator when reading and writing json data. By giving it an explicit string, you ensure your serialization system remains stable, even if the class name or location changes.
🤐 Serializing a Logic Block
Once you have a serializable logic block, it's easy to turn it into json (and get it back again).
Likewise, you can deserialize a logic block from json.
🧽 Absorbing a Logic Block
When deserializing a logic block, it can be helpful to copy the state and blackboard values from a deserialized logic block into an already existing logic block. Copying the state of a deserialized logic block into an existing logic block allows you to preserve any bindings already established.
🧑🏫 Saving Blackboard Values
Since most blackboard values represent runtime dependencies, blackboard values are not persisted by default when serializing a logic block.
You can register blackboard values that will get serialized. This is done by using the blackboard's Save()
method. Since every logic block implements the blackboard interface, we can do so easily after creating the logic block.
By requiring you to pass in a factory closure, the serialization system ensures the value is only created when needed — allowing RestoreFrom()
to provide a value instead if you call it before using an existing logic block.
⏳ Versioning
Logic blocks supports versioning states.
This results in the following state hierarchy:
The [Id]
is moved to an abstract base type and the [Version]
attribute is applied to the derived types.
You can shuffle around type hierarchies to match your versioning logic as long as the id's and shape of the types remain stable across versions.
😶🌫️ Testing Abstract Serializable States
When you give a logic block an [Id]
to make it serializable, it suddenly requires that every state be an introspective type so it can be pre-allocated and serialized correctly.
When testing abstract logic block states, it is common to create a concrete test implementation of the state for testing purposes:
Serializable logic blocks, however, require that TestState
be introspective and identifiable (since it is a concrete type). Otherwise, you'd get an error when creating the logic block.
You can simply add the [TestState]
attribute to the state to prevent logic blocks from trying to preallocate it and verify that it is serializable.
Be sure to read the Serialization package's readme for additional tips on making serializable types.