Values, owned state, and links
Chronicler separates state by ownership. That distinction keeps schemas explicit and prevents a restore pass from quietly taking control of a runtime's object lifecycle.
One schema, ordered by code
Every recordable type implements RecordData(IChronicler):
public void RecordData(IChronicler chronicler)
{
RecordValues.Look(chronicler, ref Health, "health", 100);
RecordDeep.Look(chronicler, ref Inventory, "inventory");
RecordLinks.Look(chronicler, ref Target, "target");
}
Saving and loading run this method in the same order. Field names, declared defaults, and the choice of recording helper are durable parts of the schema. The transports do not discover members with reflection or infer ownership from the object graph.
Pick a lane by ownership
Values
Use RecordValues for leaf state. Pass the canonical default explicitly:
RecordValues.Look(chronicler, ref Health, "health", 100);
When health is absent during loading, the field becomes 100. The result does
not depend on the target object's previous value.
Owned nested state
Use the deep helpers when the current object owns the nested state:
- RecordDeep for a recordable class;
- RecordDeepStruct for a non-nullable recordable struct;
- RecordNullableDeep for a nullable recordable struct.
For class state, initialize the nested object before loading:
public InventoryState Inventory = new();
public void RecordData(IChronicler chronicler)
{
RecordDeep.Look(chronicler, ref Inventory, "inventory");
}
The host remains responsible for construction, dependency injection, pooling, and runtime registration. Chronicler only transfers the recorded state.
Runtime-owned or external links
Use RecordLinks when a field points to an object that should not be serialized inline. A ChronicleContext carries the session's ChronicleLinkRegistry:
public sealed class ActorState : IRecordable
{
public RuntimeEntity? Target;
public void RecordData(IChronicler chronicler)
{
RecordLinks.Look(chronicler, ref Target, "target");
}
}
ChronicleContext context = new();
context.Links.RegisterInstance("player-42", runtimeEntity);
string json = JsonRecordSerializer.Serialize(actorState, context);
JsonRecordSerializer.Populate(restoredActorState, json, context);
The registry maps a stable ID to the runtime instance and can also use a custom
IRecordLinkResolver<T>. Optional slot values let one runtime
type use multiple independent identity domains.
Immediate links must resolve during the load pass. If the referenced object is
registered later in the graph, use RecordLinks.LookDeferred(...) with an
assignment callback. The serializer resolves queued links after the graph has
loaded and throws if any remain unresolved.
Transport behavior
JsonRecordSerializer and, in the standard package, MemoryPackRecordSerializer implement the same IChronicler contract. They both:
- call the type-owned schema directly;
- populate an existing target;
- apply declared defaults for missing values;
- use a context for stable links;
- resolve deferred links after loading.
Transport payloads are not intended to be interchangeable. The shared contract
is the state behavior expressed by RecordData(...).
State-backed JSON types
IStateBacked<TState> covers a separate System.Text.Json integration
for helper objects that expose one canonical state value. Register
StateJsonConverterFactory when each record type implements one
IStateBacked<TState> contract and has a public constructor accepting that
exact state type:
var options = new JsonSerializerOptions();
options.Converters.Add(new StateJsonConverterFactory());
The converter writes an object with a single State property and reconstructs
the helper from that state constructor. This is not the same as populating an
IRecordable runtime shell; use it only when construction from canonical state
is the intended contract.
Payload editing
SerializationPayloadEditor can remove or replace entries in a serialized payload. It is useful for compatibility tests and controlled schema migrations. Its common overloads operate on JSON in Lean builds and default to MemoryPack in the standard build, so prefer the format-specific methods when call-site clarity matters.
Package boundaries
| Package | Public surface |
|---|---|
Chronicler.Core |
Core recording APIs, JSON, MemoryPack, record hashes, link services, and payload editing for both built-in formats. |
Chronicler.Core.Lean |
Core recording APIs, JSON, record hashes, link services, and JSON payload editing. MemoryPack-specific source is compiled out. |
Chronicler.MemoryPackShim |
Compatibility attributes in the MemoryPack namespace for annotated Lean assemblies. It does not serialize data. |
Continue with Deterministic record hashes when the goal is state comparison rather than transport.