Payload Formats
Encode text or JSON, validate received data, and adapt a serializer with no change to the SDK.
Before you start: obtain a connected chat segment with the setup from Messaging. Each format below is an alternative. Both the sender and the receiver must agree on the format.
1. Start with text
stopText := chat.OnMessage(func(payload []byte, _ celeris.MessageMetadata) {
if text, err := celeris.ReadText(payload); err == nil {
fmt.Println(text)
}
})
if err := chat.Publish(ctx, celeris.TextPayload("Hello")); err != nil {
log.Println(err)
}
// Later, when this receiver is no longer needed:
stopText()
Celeris carries opaque bytes. It does not infer a content type, compress JSON automatically, or translate formats for recipients. TextPayload replaces invalid UTF-8 with U+FFFD. ReadText returns ErrConfiguration for bytes that are not valid UTF-8.
2. Send and validate JSON
ReadJSON[T] decodes into the type that you name. That type is an assertion, not a validation. The decoder ignores unknown fields, and absent fields stay zero. Validate anything received from a publisher that you do not control:
type typingEvent struct {
Type string `json:"type"`
Active bool `json:"active"`
}
stopJSON := chat.OnMessage(func(payload []byte, _ celeris.MessageMetadata) {
event, err := celeris.ReadJSON[typingEvent](payload)
if err != nil || event.Type != "typing" {
fmt.Println("Ignored an invalid typing event.")
return
}
fmt.Println("Peer typing:", event.Active)
})
payload, err := celeris.JSONPayload(typingEvent{Type: "typing", Active: true})
if err != nil {
log.Fatal(err)
}
if err := chat.Publish(ctx, payload); err != nil {
log.Println(err)
}
JSONPayload writes compact JSON with encoding/json, without HTML escapes or a trailing newline. Struct fields keep their declaration order, and the encoder sorts map keys. Compare decoded values across SDKs, not bytes. JSONPayload returns ErrConfiguration for values that JSON cannot represent: NaN, infinities, channels, functions, complex numbers, and cyclic structures. ReadJSON returns ErrConfiguration for invalid UTF-8, invalid JSON, or JSON that does not fit T. Render received text safely. Do not insert it as HTML.
3. Bring your own serializer
The SDK includes one adapter, not a separate bundled library for every format. NewPayloadCodec takes an encoder that returns []byte, and a decoder. It returns ErrConfiguration if either function is nil. Errors from your encoder or decoder propagate unchanged.
MessagePack
go get github.com/vmihailenco/msgpack/v5
type reading struct {
Sensor string `msgpack:"sensor"`
Value float64 `msgpack:"value"`
}
codec, err := celeris.NewPayloadCodec(
func(value reading) ([]byte, error) { return msgpack.Marshal(value) },
func(payload []byte) (reading, error) {
var value reading
err := msgpack.Unmarshal(payload, &value)
return value, err
},
)
if err != nil {
log.Fatal(err)
}
stopBinary := chat.OnMessage(func(payload []byte, _ celeris.MessageMetadata) {
if value, err := codec.ReadPayload(payload); err == nil {
fmt.Println(value.Sensor, value.Value) // Validate the decoded value before trusting it.
}
})
payload, err := codec.EncodePayload(reading{Sensor: "temperature", Value: 21.5})
if err != nil {
log.Fatal(err)
}
if err := chat.Publish(ctx, payload); err != nil {
log.Println(err)
}
MessagePack keeps binary fields, and it distinguishes integers from floats. JSON cannot do this.
Protobuf
This example uses google.golang.org/protobuf/proto and the package that protoc --go_out generates from a schema such as this one:
syntax = "proto3";
package demo;
option go_package = "example.com/app/demo";
message Reading {
string sensor = 1;
double value = 2;
}
readings, err := celeris.NewPayloadCodec(
func(reading *demo.Reading) ([]byte, error) { return proto.Marshal(reading) },
func(payload []byte) (*demo.Reading, error) {
reading := &demo.Reading{}
err := proto.Unmarshal(payload, reading)
return reading, err
},
)
if err != nil {
log.Fatal(err)
}
payload, err := readings.EncodePayload(&demo.Reading{Sensor: "temperature", Value: 21.5})
Check err. Then publish payload as in the MessagePack example. The numbers in the schema identify fields on the wire. Keep them stable when you change your schema. A Go receiver and a JavaScript or Python sender interoperate when all parties use the same schema.
Mix text, JSON, MessagePack, and Protobuf receivers on one segment only when your application can distinguish the formats. Separate segments or an explicit envelope can provide that distinction. A successful decode does not prove who sent the message.
4. Handle timestamps
The SDK's metadata and your message payload are separate. Your serializer decides the types inside a payload. The SDK uses these Go types for its own values:
| Value | Go type |
|---|---|
Presence request page and perPage | int32 |
Presence page Total, PerPage, CurrentPage, From, To | int32 |
ConnectTimeout and PresenceQueryTimeout | time.Duration |
Credential request DisconnectedAt and ReplayLookback | time.Time and time.Duration, zero on the first attempt |
Recovery event RetryIndex | int |
Message, notice, presence-event, and presence-record Timestamp | int64, Unix milliseconds |
Convert a timestamp to a time.Time value directly. The timestamp serializes to JSON as a number with no loss:
stopMetadata := chat.OnMessage(func(_ []byte, metadata celeris.MessageMetadata) {
sentAt := time.UnixMilli(metadata.Timestamp).UTC()
fmt.Println(sentAt.Format(time.RFC3339Nano))
encoded, err := json.Marshal(map[string]any{"message_id": metadata.MessageID, "timestamp": metadata.Timestamp})
if err == nil {
fmt.Println(string(encoded))
}
})
A JavaScript consumer that needs the exact value must read large timestamps as bigint values. Do not log sensitive payloads, identities, or error resources in production.
Next: Presence. For format tradeoffs, see Binary Messaging.