Payload conversion
Payload Converters serialize your application objects into a Payload and deserialize them back.
A Payload is a binary form with metadata that Temporal uses to transport data.
By default, Temporal uses a Payload Converter that handles null, byte arrays, protobuf messages, and anything JSON-serializable.
You only need a custom Payload Converter when your application uses types that aren't natively supported.
Default supported types
The default Data Converter supports converting multiple types including:
null- Byte arrays
- Protobuf JSON: if a value is an instance of a Protobuf message, it is encoded with proto3 JSON
- Jackson JSON
- Anything that can be converted to JSON
Using custom Payload conversion
Temporal SDKs provide a Payload Converter that can be customized to convert a custom data type to Payload and back.
Implementing custom Payload conversion is optional. It is needed only if the default Data Converter does not support your custom values.
To support custom Payload conversion, create a custom Payload Converter and configure the Data Converter to use it in your Client options.
The order in which your encoding Payload Converters are applied depend on the order given to the Data Converter. You can set multiple encoding Payload Converters to run your conversions. When the Data Converter receives a value for conversion, it passes through each Payload Converter in sequence until the converter that handles the data type does the conversion.
Payload Converters can be customized independently of a Payload Codec. Temporal's Converter architecture looks like this:

Temporal converter architecture
Create a custom implementation of a PayloadConverter interface and use the withPayloadConverterOverrides method to implement the custom object conversion with DefaultDataConverter.
PayloadConverter serializes and deserializes method parameters that need to be sent over the wire.
You can create a custom implementation of PayloadConverter for custom formats.
withPayloadConverterOverrides replaces the default converter that has the same encoding type, in the same position.
A converter with a new encoding type is added after the default converters instead.
The default Jackson converter accepts almost any object, so a converter with a new encoding type only receives values
that Jackson can't serialize.
To change how a JSON value is serialized, use the json/plain encoding type and delegate every value you don't
handle to JacksonJsonPayloadConverter.
The following example writes a Money value as a JSON string such as "12.34 USD" and reads it back.
Money has no default constructor, so the default Jackson converter can serialize it but can't deserialize it:
/** Payload Converter that writes Money as a JSON string such as "12.34 USD". */
public class MoneyPayloadConverter implements PayloadConverter {
// This converter replaces the default "json/plain" converter, so it delegates every
// other value to Jackson.
private final JacksonJsonPayloadConverter jackson = new JacksonJsonPayloadConverter();
@Override
public String getEncodingType() {
return "json/plain"; // The encoding type determines which default conversion behavior to override.
}
@Override
public Optional<Payload> toData(Object value) throws DataConverterException {
if (!(value instanceof Money money)) {
return jackson.toData(value);
}
String json = "\"" + money + "\"";
return Optional.of(
Payload.newBuilder()
.putMetadata(EncodingKeys.METADATA_ENCODING_KEY, ByteString.copyFromUtf8(getEncodingType()))
.setData(ByteString.copyFrom(json, StandardCharsets.UTF_8))
.build());
}
@Override
public <T> T fromData(Payload content, Class<T> valueClass, Type valueType)
throws DataConverterException {
if (valueClass != Money.class) {
return jackson.fromData(content, valueClass, valueType);
}
String text = jackson.fromData(content, String.class, String.class);
return valueClass.cast(Money.parse(text));
}
}
You can also use specific implementation classes provided in the Java SDK.
For example, to create a custom JacksonJsonPayloadConverter, use the following.
Start from JacksonJsonPayloadConverter.newDefaultObjectMapper() rather than new ObjectMapper(): the default mapper
registers the modules for Java time types and Optional, and a plain ObjectMapper fails to serialize an Instant.
public static JacksonJsonPayloadConverter yourCustomJacksonJsonPayloadConverter() {
// Start from the SDK's default ObjectMapper so Java time types, Optional, and
// field visibility keep working, then add your changes.
ObjectMapper objectMapper = JacksonJsonPayloadConverter.newDefaultObjectMapper();
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
return new JacksonJsonPayloadConverter(objectMapper);
}
To set your custom Payload Converter, use it with withPayloadConverterOverrides with a new instance of DefaultDataConverter in your WorkflowClient options that you use in your Worker process and to start your Workflow Executions.
The following example sets the MoneyPayloadConverter Payload Converter.
DefaultDataConverter ddc =
DefaultDataConverter.newDefaultInstance()
.withPayloadConverterOverrides(new MoneyPayloadConverter());
WorkflowClientOptions workflowClientOptions =
WorkflowClientOptions.newBuilder().setDataConverter(ddc).build();