Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.goakt.dev/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Messages crossing process boundaries must be serialized. GoAkt v4 ships three pluggable serializers and supports custom implementations via the Serializer interface.
SerializerWire formatBest for
ProtoSerializerprotobuf (default)proto.Message types — schema-driven, compact, language-portable.
CBORSerializerCBOR (binary)Plain Go structs — compact binary, faster than JSON, schema-less.
JSONSerializerJSON (via sonic)Plain Go structs where interoperability or human-readability is wanted.
All three use the same length-prefixed self-describing frame, so a custom implementation that follows the same pattern can interoperate.

ProtoSerializer (default)

Protobuf messages use the default serializer automatically. No registration needed for proto.Message types — NewConfig registers ProtoSerializer for the proto.Message interface, and proto’s own protoregistry.GlobalTypes (populated by generated init() functions) resolves the concrete type on the receiver.

CBOR for plain Go structs

For plain Go structs, use remote.WithSerializers(new(MyMessage), remote.NewCBORSerializer()) when creating the remote config. Types are registered automatically in the type registry. Both sender and receiver must register the same types via WithSerializers; for receive-only types, register them the same way — the type is auto-registered for deserialization. For bulk registration, remote.WithSerializables(new(MyMessage), new(OtherMessage)) registers each type with a shared CBOR serializer instance.

JSON for plain Go structs

JSON works identically to CBOR — same registration path, same wire frame, different encoding.
cfg := remote.NewConfig("0.0.0.0", 9000,
    remote.WithSerializers(new(MyMessage), remote.NewJSONSerializer()),
)
For bulk registration, use the parallel convenience option:
cfg := remote.NewConfig("0.0.0.0", 9000,
    remote.WithJSONSerializables(new(EventA), new(EventB), (*MyInterface)(nil)),
)
JSONSerializer is backed by bytedance/sonic configured for maximum throughput (sonic.ConfigFastest — no HTML escaping, no JSON-marshaler validation). On amd64 and arm64 sonic uses JIT-accelerated fast paths; on other architectures it transparently falls back to encoding/json.

CBOR vs JSON

Pick CBOR when both ends are goAkt and you want the smallest payloads and fastest decode. Pick JSON when payloads need to be human-readable in logs, inspected with jq, or consumed by non-goAkt tooling. Both share the same registry plumbing — switching is a one-line change in your config.

How Go types are magically serialized

When you pass a concrete Go type to WithSerializers with CBORSerializer or JSONSerializer, the type is automatically registered in a global type registry. There is no separate registration step — no RegisterSerializableTypes or similar. One line does it all:
cfg := remote.NewConfig("0.0.0.0", 9000,
    remote.WithSerializers(new(MyMessage), remote.NewCBORSerializer()),
)
Under the hood:
  1. On config build: When WithSerializers(new(MyMessage), serializer) is applied, GoAkt detects that the serializer uses the registry (CBOR or JSON) and the type is a concrete non-proto struct. It registers MyMessage in a global type registry keyed by the type’s reflected name.
  2. On serialize: When a message is sent, the serializer looks up the type name in the registry, encodes the value as CBOR or JSON, and prepends a self-describing frame (total length, type name length, type name, payload). The receiver can reconstruct the exact Go type from the type name.
  3. On deserialize: When bytes arrive, the frame header is parsed, the type name is extracted, and the registry is consulted to resolve the concrete Go type. A new instance is allocated and the payload is unmarshaled into it.
Both sender and receiver must register the same types via WithSerializers. For types you only receive (never send), register them the same way — the type is auto-registered for deserialization. Proto message types are excluded from this registry; they use protobuf’s own type resolution.

The Serializer interface

Custom serializers must implement:
package remote

type Serializer interface {
    Serialize(message any) ([]byte, error)
    Deserialize(data []byte) (any, error)
}
MethodPurpose
SerializeEncode a message into bytes. The encoding must be self-describing so the receiver can reconstruct the concrete type without out-of-band coordination.
DeserializeDecode bytes back into the original Go value. The dynamic type must match what was passed to Serialize.
Implementations must be safe for concurrent use. Register via WithSerializers(msgType, serializer) on the remote config.

Wire format

Frames use: total length, type name length, type name, payload. All three built-in serializers share this layout, so a custom serializer that follows the same pattern interoperates at the frame level. Both client and server must use compatible serializers and compression.