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
Registration textListener =
chat.onMessage(
(payload, metadata) -> {
try {
System.out.println(Payloads.readText(payload));
} catch (CelerisException invalid) {
System.out.println("Ignored a payload that is not UTF-8 text.");
}
});
chat.publish(Payloads.text("Hello")).join();
// Later, when this receiver is no longer needed:
textListener.close();
Celeris carries opaque bytes. It does not infer a content type, compress JSON automatically, or translate formats for recipients. Payloads.text encodes UTF-8 and replaces an unpaired surrogate with U+FFFD. Payloads.readText throws a CelerisException with CONFIGURATION for bytes that are not valid UTF-8.
2. Send and validate JSON
The JDK has no JSON library, and the SDK bundles none. JSON goes through a PayloadCodec that you build from the library that you already use. This example uses Jackson 3 (tools.jackson.core:jackson-databind) and a mapper such as JsonMapper.builder().build():
record TypingEvent(String type, boolean active) {}
PayloadCodec<TypingEvent> typingEvents =
PayloadCodec.of(
mapper::writeValueAsBytes, payload -> mapper.readValue(payload, TypingEvent.class));
Registration jsonListener =
chat.onMessage(
(payload, metadata) -> {
TypingEvent event;
try {
event = typingEvents.readPayload(payload);
} catch (JacksonException invalid) {
System.out.println("Ignored an invalid typing event.");
return;
}
if (!"typing".equals(event.type())) {
System.out.println("Ignored an invalid typing event.");
return;
}
System.out.println("Peer typing: " + event.active());
});
chat.publish(typingEvents.encodePayload(new TypingEvent("typing", true))).join();
With Gson (com.google.code.gson:gson) and a gson such as new Gson(), only the codec changes:
record TypingEvent(String type, boolean active) {}
PayloadCodec<TypingEvent> typingEvents =
PayloadCodec.of(
value -> gson.toJson(value).getBytes(StandardCharsets.UTF_8),
payload -> gson.fromJson(new String(payload, StandardCharsets.UTF_8), TypingEvent.class));
The codec's own exceptions propagate unchanged: Jackson's JacksonException, or Gson's JsonParseException, which the listener catches in place of JacksonException. A decode into a record asserts a type. It does not validate the data. With default settings, both libraries ignore unknown fields and leave absent ones null, false, or zero. Validate anything received from a publisher that you do not control.
Libraries differ in field order and spacing, so compare decoded values across SDKs, not bytes. 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. PayloadCodec.of takes an encoder that returns byte[] and a decoder, and it throws CONFIGURATION if either is null. Both are java.util.function.Functions. Wrap a checked exception from a library method in an unchecked one. Exceptions from your encoder or decoder propagate unchanged.
MessagePack
This example uses org.msgpack:jackson-dataformat-msgpack. That artifact gives a Jackson 2 com.fasterxml.jackson.databind.ObjectMapper the MessagePack format through org.msgpack.jackson.dataformat.MessagePackFactory:
record Reading(String sensor, double value) {}
ObjectMapper messagePack = new ObjectMapper(new MessagePackFactory());
PayloadCodec<Reading> readings =
PayloadCodec.of(
value -> {
try {
return messagePack.writeValueAsBytes(value);
} catch (IOException failure) {
throw new UncheckedIOException(failure);
}
},
payload -> {
try {
return messagePack.readValue(payload, Reading.class);
} catch (IOException failure) {
throw new UncheckedIOException(failure);
}
});
Registration binaryListener =
chat.onMessage(
(payload, metadata) -> {
try {
Reading reading = readings.readPayload(payload);
System.out.println(reading.sensor() + " " + reading.value()); // Validate it before trusting it.
} catch (UncheckedIOException invalid) {
System.out.println("Ignored an invalid reading.");
}
});
chat.publish(readings.encodePayload(new Reading("temperature", 21.5))).join();
MessagePack keeps binary fields and distinguishes integers from floats. JSON cannot do either.
Protobuf
This example uses com.google.protobuf:protobuf-java and the class that protoc --java_out generates from a schema such as this one:
syntax = "proto3";
package demo;
option java_package = "com.example.demo";
option java_multiple_files = true;
message Reading {
string sensor = 1;
double value = 2;
}
PayloadCodec<Reading> readings =
PayloadCodec.of(
Reading::toByteArray,
payload -> {
try {
return Reading.parseFrom(payload);
} catch (InvalidProtocolBufferException invalid) {
throw new IllegalArgumentException("Payload is not a Reading message.", invalid);
}
});
byte[] payload =
readings.encodePayload(Reading.newBuilder().setSensor("temperature").setValue(21.5).build());
Here Reading is the generated com.example.demo.Reading. Publish payload and read it as in the MessagePack example. The numbers in the schema identify fields on the wire. Keep them stable when you change your schema. A Java receiver and a JavaScript, Python, or Go sender interoperate when all of them 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. Keep authorization and application validation in place.
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 Java types for its own values:
| Value | Java type |
|---|---|
Presence request page and perPage | int |
Presence page total, perPage, currentPage, from, to | int |
connectTimeout and presenceQueryTimeout | Duration |
Credential request disconnectedAt and replayLookback | Optional<Instant> and Optional<Duration>, empty on the first attempt |
Recovery event retryIndex | int |
Message, notice, presence-event and presence-record timestamp | long, Unix ms |
A timestamp converts directly to an Instant and serializes to JSON as a number with no loss:
Registration metadataListener =
chat.onMessage(
(payload, metadata) -> {
Instant sentAt = Instant.ofEpochMilli(metadata.timestamp());
System.out.println(sentAt); // ISO 8601, in UTC
String encoded =
mapper.writeValueAsString(
Map.of("message_id", metadata.messageId(), "timestamp", metadata.timestamp()));
System.out.println(encoded);
});
A JavaScript consumer of that JSON must read large timestamps as bigint when it needs their exact value. Do not log sensitive payloads, identities, or error resources in production.
Next: Presence. For format tradeoffs, see Binary Messaging.