Documentation

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:

reading.proto
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:

ValueJava type
Presence request page and perPageint
Presence page total, perPage, currentPage, from, toint
connectTimeout and presenceQueryTimeoutDuration
Credential request disconnectedAt and replayLookbackOptional<Instant> and Optional<Duration>, empty on the first attempt
Recovery event retryIndexint
Message, notice, presence-event and presence-record timestamplong, 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.

We use Google cookies for analytics and for advertising measurement, only if you allow them. Until you choose, Google sets no cookies. Refer to our Cookie Policy.