Serialization overview
このコンテンツはまだ日本語訳がありません。
Two scenarios require turning JS values into bytes:
- Cluster wire — a
tellto a remote actor needs the message serialized for TCP transmission. - Persistence — events / snapshots / durable state stored on disk or in a journal.
The framework’s Serializer interface is the abstraction.
Two built-ins ship:
| Serializer | Format | When |
|---|---|---|
JsonSerializer | JSON | Default. Human-debuggable, widely understood. |
CborSerializer | CBOR (RFC 8949) | Compact, binary-native, faster. |
Two more take a schema you bring — the schema library
(avsc, protobufjs, generated code) stays a dependency of your
project:
| Serializer | Format | When |
|---|---|---|
AvroSerializer | Avro | Smallest rows; schema-registry interop. |
ProtobufSerializer | Protobuf | Cross-language contracts; forward-compatible wire. |
Plus an extension point: implement Serializer<T> for
MessagePack, FlatBuffers, or anything else.
How it works
Section titled “How it works”For a cross-node tell, the cluster wraps the message in an
envelope and sends it as a length-prefixed JSON.stringify
frame — the pluggable Serializer is not consulted on the wire:
Sender side: remoteRef.tell(message) → wrapped in an envelope { to, from, body, tag } → ActorRefs embedded in body → wire-safe markers → JSON.stringify(envelope) → length-prefixed frame → cluster transport sends over the wire
Receiver side: cluster transport reassembles the frame → JSON.parse(frame) → envelope → ActorRef markers → live remote refs → body delivered to the actor's onReceiveThe body must be JSON-safe. The only type hint on the wire
is tag — the message’s constructor name — for routing; there is
no serializerId / manifest framing on the cluster wire.
The Serializer interface
Section titled “The Serializer interface”interface Serializer<T = unknown> { readonly id: number; // unique per serializer readonly name: string; // human-readable includesManifest: boolean; manifest(obj: T): string; // type tag toBinary(obj: T): Uint8Array; fromBinary(bytes: Uint8Array, manifest: string): T;}Small surface — encode, decode, identify. The framework’s
SerializationExtension is a registry that maps classes to
serializers (via bind); when you encode a value through the
extension (ext.encode), it looks up the serializer bound to the
value’s class.
Default behavior
Section titled “Default behavior”const system = ActorSystem.create('my-app');// → JsonSerializer registered as default// → Every value serializes as JSON unless a specific serializer is boundIf you do nothing, every value’s bytes are JSON. Plain objects, arrays, strings, numbers — all work. Class instances serialize as plain objects (losing methods).
Picking JSON or CBOR
Section titled “Picking JSON or CBOR”| Aspect | JSON | CBOR |
|---|---|---|
| Wire size | Larger | ~20-40 % smaller |
| Speed | Slower than CBOR for binary data | Faster |
| Debugging | Trivial (text) | Requires CBOR-aware tool |
| Binary fields | base64-encoded (wasteful) | Native binary |
| Library support | Universal | Solid in JS land |
For most apps, JSON is fine — the perf difference doesn’t matter and the debuggability is valuable.
For bandwidth-sensitive cases (large cluster, lots of events, IoT-style payloads), CBOR wins meaningfully.
See JsonSerializer + CborSerializer for details.
Schema serializers and custom formats
Section titled “Schema serializers and custom formats”Avro and Protobuf ship ready-made — you supply the compiled schema, the framework supplies the serializer:
import { ProtobufSerializer, ProtobufSerializerOptions, SerializationExtensionId } from 'actor-ts';
const protobufOptions = ProtobufSerializerOptions.create<MyEvent>() .withMessageType(root.lookupType('shop.MyEvent')) .withId(100);const serializer = new ProtobufSerializer(protobufOptions);
const ext = system.extension(SerializationExtensionId);ext.register(serializer); // add it to the id lookup tableext.bind(MyEvent, serializer.id); // route MyEvent through itbind takes a numeric serializer id, so the serializer must
be registered first. Now the extension resolves every MyEvent
value to Protobuf; other types fall back to JSON.
For any other format — MessagePack, FlatBuffers, something
bespoke — implement Serializer<T> yourself. Ids 1–99 are
reserved for the built-ins; pick ≥ 100.
See Custom serializers for the full setup.
What can be serialized
Section titled “What can be serialized”The JsonSerializer class handles:
- Plain objects (
{ a: 1, b: 'two' }). - Arrays.
- Strings, numbers, booleans,
null. - Nested combinations of the above.
Date,Uint8Array,Map,Set, andbigint— round-tripped as real instances via type tags.NaN/Infinity/-0,RegExp,URL,Error(name + message + cause, no stack), and every typed array /DataView/ArrayBuffer(#889).
It throws on functions, symbols, undefined, circular
references, Promise, and WeakMap / WeakSet; class
instances decode to plain objects (methods + identity lost).
CborSerializer carries the same list — a shared test suite
asserts the two agree on every type — just as compact binary
instead of tagged JSON text. It differs in one place: undefined
is carried natively rather than rejected, which makes it the more
permissive of the two.
The cluster wire is different: cross-node messages go through
raw JSON.stringify (not JsonSerializer), so the usual JSON
limits apply on the wire — Date → ISO string, Map / Set →
{}, bigint throws, undefined / functions dropped. Convert
to plain, JSON-safe shapes before a cross-node tell:
// ✗ref.tell({ when: new Date() });
// ✓ref.tell({ when: Date.now() });Where serialization fires
Section titled “Where serialization fires”1. cluster wire — every cross-node tell (raw JSON.stringify).2. PersistentActor.persist(event) — events to the journal.3. DurableStateActor.persist(state) — state to the store.4. Snapshot writes — snapshots to the snapshot store.5. DistributedData — replicated state across the cluster.Persistence (2-4) writes the tagged JSON tree format — the same
tree JsonSerializer uses — so Date / Map / Set / bigint /
Uint8Array round-trip through every journal, snapshot store and
durable-state store without configuration. Each store also accepts
an explicit withSerializer(...) option that routes a custom
Serializer into its rows (see
custom serializers);
the SerializationExtension registry bindings are still not
consulted for persistence or the wire — that wiring is tracked in
#450.
The cluster wire uses JSON.stringify on the envelope directly —
not the pluggable Serializer (see How it works). In-process
tells don’t serialize — the receiver gets the exact in-memory
reference. Serialization happens only at process / disk /
cluster boundaries.
Where to next
Section titled “Where to next”- JSON serializer — the default.
- CBOR serializer — the binary alternative.
- Custom serializers — Protobuf, Avro, etc.
- Messages — what shapes survive serialization.
- Refs across nodes — the cluster wire that consumes serializers.
