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
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 automatically, 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 received from a publisher that you do not control. One option is Pydantic, which the client already depends on:
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). Compare decoded values across SDKs, not the bytes. json_payload raises ConfigurationError for values that JSON cannot represent: NaN, infinities, sets, bytes, other non-JSON types, and circular structures. read_json raises ConfigurationError for invalid UTF-8 or malformed JSON. 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. Your encoder must return bytes. Errors from 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
This example uses classes that protoc generated from a schema such as this one:
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 when you change your schema. A Python receiver and a JavaScript sender interoperate when both 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. All of the SDK's own numeric fields are int values:
| 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 that needs the exact value must read large timestamps from that JSON as bigint. Do not log sensitive payloads, identities, or error resources in production.
Next: Presence. For format tradeoffs, see Binary Messaging.