JSON serializer
JsonSerializer is the default serializer. It wraps
JSON.stringify + JSON.parse (bytes are UTF-8) with a tree
walk that also round-trips Date, Uint8Array, Map, Set,
and bigint via type tags.
import { JsonSerializer } from 'actor-ts';
const serializer = new JsonSerializer();const bytes = serializer.toBinary({ kind: 'increment', n: 1 });// → Uint8Array of '{"kind":"increment","n":1}' as UTF-8
const decoded = serializer.fromBinary(bytes, '');// → { kind: 'increment', n: 1 }Why JSON is the default
Section titled “Why JSON is the default”- Universal — every language has a JSON parser.
- Human-readable — open a journal file with
cat; see the events. - Schema-flexible — no compile step; types evolve via ordinary code changes (+ schema-migration adapters).
- Battle-tested — every Web API speaks JSON.
For most apps, you’ll never change serializers — JSON covers it.
What it handles
Section titled “What it handles”- Plain objects, arrays.
- Strings, numbers, booleans,
null. - Nested combinations.
Date,Uint8Array,Map,Set, andbigint— round-tripped back into real instances via type tags (__date__,__bytes__,__map__,__set__,__bigint__).BidirectionalMapandBidirectionalMultiMap— the two framework classes with tags of their own (__bidirectionalmap__, #1035;__bidirectionalmultimap__, #1037), so they can be held in persistent state without an adapter. Only the forward direction is written — pairs for the first, an adjacency list for the second; the inverse is rebuilt on decode.NaN/Infinity/-Infinity/-0,RegExp(source + flags),URL,Error(name + message + cause — no stack), and every typed array /DataView/ArrayBuffer— round-tripped via type tags too (#889).Number/String/Booleanwrapper objects unwrap to their primitive, likeJSON.stringify.toJSON()— honoured likeJSON.stringifydoes, so Luxon/Temporal wrappers and your owntoJSONtypes encode as their JSON form.- User data that looks like a tag — an object whose sole key is a
reserved tag name — is wrapped in a
__literal__escape on encode and round-trips as plain data. A tag is only ever interpreted when it is an object’s sole own key.
Persistence uses exactly this tree format: every journal, snapshot store and durable-state store writes its payloads through the same tagged tree (see what events and state may contain).
What needs care:
| Type | Behavior |
|---|---|
undefined | Throws a SerializationError (stricter than raw JSON.stringify, which drops it). The persistence payload codec instead drops undefined object properties like JSON.stringify and preserves undefined in value positions (array slots, Set members, Map entries) via a tag (#889). CborSerializer carries it natively everywhere, keys included — the one place the two codecs differ (#1036). |
Function / Symbol / Promise / WeakMap / WeakSet | Throw a SerializationError — inherently non-serialisable. |
| Circular references | Throw a SerializationError naming the key path (instead of overflowing the stack). |
| Class instances | toJSON() is used when present; otherwise decoded as plain { ...fields } — methods + class identity lost. |
Date / Map / Set / bigint / Uint8Array | Round-trip as real instances, but the JSON carries an actor-ts-specific tag shape (e.g. {"__date__":"…"}) — a plain (non-actor-ts) JSON reader won’t see a bare value. |
For most actor messages, this is fine — the framework’s message conventions already discourage non-serializable shapes.
JSON is larger than binary formats for the same data:
- Numbers are decimal strings (
"42"is 4 bytes vs CBOR’s 2). - Field names are repeated (every record has
"kind":"..."). - Binary data (
Uint8Array) base64-encodes (33 % overhead).
For text-heavy or repetitive data, JSON’s overhead is small. For binary-heavy data (images, encoded payloads), CBOR saves meaningfully.
Performance
Section titled “Performance”Rough numbers per serialize/deserialize pair:
- Small object (~50 bytes): ~1-2 microseconds.
- Medium object (~1 KB): ~5-15 microseconds.
- Large object (~100 KB): ~1-2 ms.
JSON.parse / JSON.stringify are native code in V8/JSC —
fast enough that JSON serialization is rarely the bottleneck.
When to switch to something else
Section titled “When to switch to something else”Need to interop with non-JSON systems? → Custom (Protobuf/Avro)Bandwidth-bound (large or many small messages)? → CBORNeed typed schemas in source control? → Custom (Protobuf)Otherwise → Stay on JSONMost production apps stay on JSON for the actor wire and persistence — the perf / size gains from switching rarely justify the operational complexity.
Where to next
Section titled “Where to next”- Serialization overview — the bigger picture.
- CBOR serializer — the binary alternative.
- Custom serializers — Protobuf / Avro / etc.
- Messages — the conventions for serialize-friendly shapes.
