コンテンツにスキップ
日本語

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 }
  • 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.

  • Plain objects, arrays.
  • Strings, numbers, booleans, null.
  • Nested combinations.
  • Date, Uint8Array, Map, Set, and bigint — round-tripped back into real instances via type tags (__date__, __bytes__, __map__, __set__, __bigint__).
  • BidirectionalMap and BidirectionalMultiMap — 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/Boolean wrapper objects unwrap to their primitive, like JSON.stringify.
  • toJSON() — honoured like JSON.stringify does, so Luxon/Temporal wrappers and your own toJSON types 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:

TypeBehavior
undefinedThrows 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 / WeakSetThrow a SerializationError — inherently non-serialisable.
Circular referencesThrow a SerializationError naming the key path (instead of overflowing the stack).
Class instancestoJSON() is used when present; otherwise decoded as plain { ...fields } — methods + class identity lost.
Date / Map / Set / bigint / Uint8ArrayRound-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.

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.

Need to interop with non-JSON systems? → Custom (Protobuf/Avro)
Bandwidth-bound (large or many small messages)? → CBOR
Need typed schemas in source control? → Custom (Protobuf)
Otherwise → Stay on JSON

Most production apps stay on JSON for the actor wire and persistence — the perf / size gains from switching rarely justify the operational complexity.