Payload Formats
Encode text or JSON, validate received data, and adapt a serializer without changing the SDK.
Before you start: obtain a connected chat segment using Messaging. Each format below is an alternative: sender and receiver must agree on the format.
1. Start with text
from useceleris_client import read_text, text_payload
stop_text = chat.on_message(lambda payload, metadata: print(read_text(payload)))
await chat.publish(text_payload("Hello"))
# Later, when this receiver is no longer needed:
stop_text()
Celeris carries opaque bytes. It does not infer a content type, compress JSON, or translate formats for recipients. read_text raises ConfigurationError for bytes that are not valid UTF-8.
2. Send and validate JSON
read_json returns whatever the JSON holds. Validate anything from a publisher you do not control; Pydantic, which the client already depends on, is one way:
from typing import Literal
from pydantic import BaseModel, ValidationError
from useceleris_client import ConfigurationError, MessageMetadata, json_payload, read_json
class Typing(BaseModel):
type: Literal["typing"]
active: bool
def receive(payload: bytes, metadata: MessageMetadata) -> None:
try:
event = Typing.model_validate(read_json(payload))
except (ConfigurationError, ValidationError):
print("Ignored an invalid typing event.")
return
print("Peer typing:", event.active)
stop_json = chat.on_message(receive)
await chat.publish(json_payload({"type": "typing", "active": True}))
json_payload writes compact JSON, as JavaScript's JSON.stringify does. Floats keep Python's spelling (1.0, 1e-07 where JavaScript writes 1, 1e-7), so compare decoded values across SDKs, not bytes. It raises ConfigurationError for values JSON cannot represent: NaN, infinities, sets, bytes, other non-JSON types and circular structures. read_json raises ConfigurationError for invalid UTF-8 or invalid JSON. Render received text safely rather than as HTML.
3. Bring your own serializer
The SDK includes one adapter rather than a bundled library for every format. Your encoder must return bytes. Errors raised by your encoder or decoder propagate unchanged.
MessagePack
pip install msgpack
import msgpack
from useceleris_client import MessageMetadata, create_payload_codec
codec = create_payload_codec(
encode=lambda value: msgpack.packb(value),
decode=lambda payload: msgpack.unpackb(payload),
)
def receive(payload: bytes, metadata: MessageMetadata) -> None:
reading = codec.read_payload(payload)
print(reading) # Validate the decoded value before trusting it.
stop_binary = chat.on_message(receive)
await chat.publish(codec.encode_payload({"sensor": "temperature", "value": 21.5}))
MessagePack keeps binary fields and distinguishes integers from floats, which JSON cannot.
Protobuf
With classes generated by protoc from a schema such as:
syntax = "proto3";
package demo;
message Reading {
string sensor = 1;
double value = 2;
}
from reading_pb2 import Reading # generated by protoc --python_out
from useceleris_client import create_payload_codec
readings = create_payload_codec(
encode=lambda reading: reading.SerializeToString(),
decode=Reading.FromString,
)
await chat.publish(readings.encode_payload(Reading(sensor="temperature", value=21.5)))
The numbers in the schema identify fields on the wire; keep them stable as the schema evolves. A Python receiver and a JavaScript sender interoperate as long as both use the same schema.
Do not mix text, JSON, MessagePack and Protobuf receivers on one segment unless your application defines how to tell them apart; separate segments or an explicit envelope can. Successfully decoding a payload does not prove who sent it.
4. Handle timestamps
The SDK's metadata and your payload are separate: your serializer decides the types inside a payload. The SDK's own numbers are all int:
| Value | Python type |
|---|---|
Presence request page and per_page | int |
Presence result total, per_page, current_page, from_, to | int |
| Connection and presence-query timeouts, in milliseconds | int |
Credential request disconnected_at and replay_lookback_ms | int or None |
Recovery event retry_index | int |
Message, notice, presence-event and presence-record timestamp | int, Unix ms |
A timestamp converts to a datetime directly, and serializes to JSON as a number with no loss:
import json
from datetime import datetime, timezone
from useceleris_client import MessageMetadata
def record(payload: bytes, metadata: MessageMetadata) -> None:
sent_at = datetime.fromtimestamp(metadata.timestamp / 1000, tz=timezone.utc)
print(sent_at.isoformat())
print(json.dumps({"message_id": metadata.message_id, "timestamp": metadata.timestamp}))
stop_metadata = chat.on_message(record)
A JavaScript consumer of that JSON should read large timestamps as bigint if it needs their exact value. Do not log sensitive payloads, identities or error resources in production.
Next: Presence. For format tradeoffs, see Binary Messaging.