Persistence (Serialization)
Page contents
The library provides two built-in strategies for persisting and restoring a state machine, both serializable to JSON via kotlinx.serialization:
| Strategy | API | Size | Listeners on restore | Best for |
|---|---|---|---|---|
| State snapshot | captureSavedStateConfig() / restoreBySavedStateConfig() | Constant (active states only) | Suppressed by default | Long-running machines, frequent checkpoints |
| Event recording | machine.eventRecorder / restoreByRecordedEvents() | Grows with event count | Suppressed by default | Audit trails, full replay, debugging |
Both strategies require that the restored machine is built from identical code — only the active configuration is persisted, not the machine structure itself.
There are several kinds or levels of StateMachine persistence (serialization). Let’s look at sample use cases:
- Structure + configuration - Create
StateMachineon some process/host and send its structure and active configuration by network to another process/host. The receiver can dynamically create the sameStateMachineinstance in the same state as original one. This case currently lacks built-in support by the library (you can open an issue if you need something like that). -
Configuration only - Both original and restored
StateMachineinstances are created by identical static code (in a single or multiple different processes/hosts). Only active configuration can be saved and restored. This case in turn may be reached in three different ways:- Persisting state - (not supported) deeply serializing all internal data, active states, history stack, state variables etc. from original
StateMachineand applying them to restored one. - Active state configuration - (supported) lightweight snapshot of currently active states and
DataStatedata values. Constant size regardless of machine lifetime. Listeners fire normally during restore. See Active state configuration persistence. - Event recording - (supported) serializing all incoming events and replaying them on a new machine instance, which leads it into the same state as the original. Listeners are suppressed during replay by default. See Event recording.
- Persisting state - (not supported) deeply serializing all internal data, active states, history stack, state variables etc. from original
Event recording
The library supports event recording out of the box. To enable it you should use EventRecordingArguments in CreationArguments when creating a machine instance by createStateMachine() functions family. The recording process can be configured with EventRecordingArguments properties.
val machine = createStateMachine(
creationArguments = buildCreationArguments { eventRecordingArguments = buildEventRecordingArguments {} }
) {
// ...
}
buildEventRecordingArguments {} accepts two optional properties:
clearRecordsOnMachineRestart: Boolean(defaulttrue) — clears recorded events when the machine is stopped and started again. Disable if you want to preserve the full history across restarts.skipIgnoredEvents: Boolean(defaulttrue) — ignored events are not recorded, since they do not affect machine state and replaying them is unnecessary.
When the machine had processed your business logic events, and you want to save its state configuration, first you have to get the recorded events:
val recordedEvents = machine.eventRecorder.getRecordedEvents()
RecordedEvents object now is ready to be serialized. The library provides an implementation of serialization process using kotlinx.serialization library starting from KStateMachine version v0.32.0.
Initialize serialization format (JSON for instance) using KStateMachineSerializersModule. This module contains serialization routines for library classes.
val jsonFormat = Json {
// use special, library provided SerializersModule for RecordedEvents and its internals
// from kstatemachine-serialization artifact
serializersModule = KStateMachineSerializersModule + SerializersModule { /* ... */ }
}
And encode (serialize) RecordedEvents object:
val recordedEventsJson = jsonFormat.encodeToString(recordedEvents)
Custom serialization library
Alternatively you can use some other serialization library to serialize RecordedEvents class by your own (you will also need to serialize SerializableGeneratedEvent class for internal events generated by the library itself).
Restoring StateMachine
When a user wants to restore the StateMachine, they deserialize the RecordedEvents object and create a StateMachine instance having exactly the same structure as the original one. Typically, both instances are created by the same code.
val restoredRecordedEvents = jsonFormat.decodeFromString<RecordedEvents>(recordedEventsJson)
Calling restoreByRecordedEvents() or its blocking analog restoreByRecordedEventsBlocking() will process recorded events over just created StateMachine instance.
machine2.restoreByRecordedEvents(restoredRecordedEvents)
restoreByRecordedEvents() method will start the machine if necessary. You can configure restoration process by restoreByRecordedEvents() arguments. The machine should not process any events before its restoration (in such case exception will be thrown) as it can possibly lead to incorrect restoration result.
Configuring restoration
restoreByRecordedEvents() accepts the following parameters:
muteListeners(default:true) — whentrue, listener callbacks are suppressed during replay, since the application reactions were already executed when the original events were processed.disableStructureHashCodeCheck(default:false) — skip the machine structure integrity check. Useful when you intentionally restore on a structurally different machine, though results may differ.validator(default:StrictValidator) — called after replay to validate theRestorationResult. The library provides two built-in implementations:StrictValidator— throwsRestorationResultValidationExceptionif any warnings or failed processing results are found. This is the default and recommended choice.EmptyValidator— skips validation entirely, useful when you expect and accept warnings.- Custom
RestorationResultValidator— implement thefun interfaceto apply your own logic.
Example using EmptyValidator to allow warnings:
machine2.restoreByRecordedEvents(restoredRecordedEvents, validator = EmptyValidator)
Active state configuration persistence
An alternative to event recording is to take a snapshot of the currently active states (and their DataState data values). The snapshot has constant size regardless of how many events the machine has processed, making it ideal for long-running machines.
Restoration via restoreBySavedStateConfig() genuinely enters states using the same mechanism as Testing.startFrom(), so listener callbacks fire normally during restore — unlike restoreByRecordedEvents() which suppresses them by default.
How it works
Step 1 — capture (synchronous, no suspend needed):
val snapshot: SavedStateConfig = machine.captureSavedStateConfig()
Step 2 — restore on a freshly constructed machine with identical structure:
// suspending
machine2.restoreBySavedStateConfig(snapshot)
// or blocking
machine2.restoreBySavedStateConfigBlocking(snapshot)
restoreBySavedStateConfig() starts the machine if it has not been started yet.
Prerequisites
The following conditions are verified at capture time (an IllegalStateException is thrown if any fail):
- The machine must be running.
isUndoEnabledmust befalse— the undo stack cannot be snapshotted and would be empty after restore. PassdisableUndoEnabledCheck = trueto opt in: the restored machine starts with an empty undo stack but can record and undo events processed after restoration.- All states must have non-blank names. Use
requireNonBlankNameswithSTATESorSTATES_AND_TRANSITIONSvalue inbuildCreationArguments {}to enforce this at machine start, or assign names to every state manually.
Serialization
SavedStateConfig can be serialized using kotlinx.serialization. The library provides SavedStateConfigSerializer via KStateMachineSerializersModule (from kstatemachine-serialization artifact).
If your machine has DataState instances, you must register serializers for their value types under Any::class in the SerializersModule:
val jsonFormat = Json {
serializersModule = KStateMachineSerializersModule + SerializersModule {
polymorphic(Any::class) {
subclass(MyData::class) // your DataState value type(s)
}
}
}
val snapshotJson = jsonFormat.encodeToString(snapshot)
// later:
val restoredSnapshot = jsonFormat.decodeFromString<SavedStateConfig>(snapshotJson)
machine2.restoreBySavedStateConfig(restoredSnapshot)
Configuring restoration
restoreBySavedStateConfig() accepts one optional parameter:
disableStructureHashCodeCheck(default:false) — skip the machine structure integrity check. Useful when intentionally restoring on a structurally different machine, though results may differ.
Limitations
| Limitation | Details |
|---|---|
| History states not restored | HistoryState recorded history is not part of the snapshot; after restore it defaults to defaultState or the parent’s initial state |
| All states must have names | Identification relies on state names; capture throws if any state has a null or blank name |
isUndoEnabled must be false | The undo stack cannot be captured; capture throws if undo is enabled unless disableUndoEnabledCheck = true is passed |
| Listeners fire during restore | Unlike restoreByRecordedEvents, there is no muteListeners option — state entry is genuine |