Serialization overview
Este conteúdo não está disponível em sua língua ainda.
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 tagged JSON tree
frame — the same tree format JsonSerializer and the persistence
stores write. The pluggable Serializer is still 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 → encodeJsonTree(envelope) → JSON → length-prefixed frame → cluster transport sends over the wire
Receiver side: cluster transport reassembles the frame → JSON.parse(frame) → decodeJsonTree → envelope → ActorRef markers → live remote refs → body delivered to the actor's onReceiveSo the body may hold anything in
what can be serialized below — Date,
Map, Set, bigint, Uint8Array and the rest survive a
cross-node tell exactly as they survive a journal round-trip
(#450). Class identity is still lost: the only type hint on
the wire is tag, the message’s constructor name, and 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/serialization';
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 carries the same list: cross-node messages go
through the same tagged tree (not through JsonSerializer itself —
the wire frames the whole envelope, not just the body), so a Date
arrives as a Date and a Map as a Map (#450). What the tree
refuses it refuses on the wire too, loudly: a function or a symbol
in a message body used to be dropped silently by JSON.stringify
and is now a dropped frame with an error log line.
// ✓ — arrives as a Date, not as a stringref.tell({ when: new Date() });
// ✗ — refused, and the frame never leaves the noderef.tell({ onDone: () => {} });Where serialization fires
Section titled “Where serialization fires”1. cluster wire — every cross-node tell (tagged JSON tree).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.All five write the tagged JSON tree format — the same tree
JsonSerializer uses — so Date / Map / Set / bigint /
Uint8Array round-trip through every journal, snapshot store,
durable-state store and the cluster wire without configuration.
Persistence stores additionally accept an explicit
withSerializer(...) option that routes a custom Serializer into
their rows (see
custom serializers);
the wire has no equivalent yet, and the SerializationExtension
registry bindings are still not consulted anywhere — that
wiring is tracked in
#450.
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 — what the cluster wire actually frames.
