← Files TemporalARCHIVED FILE

skills/temporal-developer/references/java/data-handling.md

7.5 KB · Nov 9, 2026 · 00:16 UTC

↓ Download file

See the change to this file →

# Java SDK Data Handling

## Overview

The Java SDK uses data converters to serialize/deserialize workflow inputs, outputs, and activity parameters. The `DataConverter` interface controls how values are converted to and from Temporal `Payload` protobufs.

## Default Data Converter

`DefaultDataConverter` applies converters in order, using the first that accepts the value:

1. `NullPayloadConverter` — `null` values
2. `ByteArrayPayloadConverter` — `byte[]` as raw binary
3. `ProtobufJsonPayloadConverter` — Protobuf `Message` instances as JSON
4. `ProtobufPayloadConverter` — Protobuf `Message` instances as binary
5. `JacksonJsonPayloadConverter` — Everything else via Jackson `ObjectMapper`

## Jackson Integration

Use `JacksonJsonPayloadConverter` with a custom `ObjectMapper` for advanced serialization (e.g., Java 8 time module, custom serializers):

```java
ObjectMapper mapper = new ObjectMapper()
    .registerModule(new JavaTimeModule())
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

DefaultDataConverter converter = DefaultDataConverter.newDefaultInstance()
    .withPayloadConverterOverrides(
        new JacksonJsonPayloadConverter(mapper)
    );

WorkflowServiceStubs service = WorkflowServiceStubs.newLocalServiceStubs();
WorkflowClient client = WorkflowClient.newInstance(
    service,
    WorkflowClientOptions.newBuilder()
        .setDataConverter(converter)
        .build()
);
```

### Computed getters

POJOs used as Workflow or Activity parameters are handled by the default `JacksonJsonPayloadConverter` and its `ObjectMapper`. Jackson may serialize a bean getter such as `getTotalPrice()` even when it has no corresponding field or setter. If deserialization then fails with an `UnrecognizedPropertyException`, a Workflow parameter can cause a Workflow Task processing failure, while an Activity parameter can cause an Activity Task failure. Mark computed getters with `@JsonIgnore`.

## Custom Data Converter

Implement `PayloadConverter` for custom serialization:

```java
public class MyCustomPayloadConverter implements PayloadConverter {
    @Override
    public String getEncodingType() {
        return "json/my-custom";
    }

    @Override
    public Optional<Payload> toData(Object value) throws DataConverterException {
        // Return Optional.empty() if this converter doesn't handle the type
        if (!(value instanceof MyCustomType)) {
            return Optional.empty();
        }
        // Serialize to Payload
        byte[] data = serialize(value);
        return Optional.of(
            Payload.newBuilder()
                .putMetadata("encoding", ByteString.copyFromUtf8(getEncodingType()))
                .setData(ByteString.copyFrom(data))
                .build()
        );
    }

    @Override
    public <T> T fromData(Payload content, Class<T> valueClass, Type valueType)
        throws DataConverterException {
        // Deserialize from Payload
        return deserialize(content.getData().toByteArray(), valueClass);
    }
}
```

Override specific converters in the default chain:

```java
DefaultDataConverter converter = DefaultDataConverter.newDefaultInstance()
    .withPayloadConverterOverrides(new MyCustomPayloadConverter());
```

## Composition of Payload Converters

`DefaultDataConverter` holds a list of `PayloadConverter` instances tried in order. The first converter whose `toData()` returns a non-empty `Optional` wins. When using `withPayloadConverterOverrides()`, converters with matching encoding types replace existing ones.

```java
DefaultDataConverter converter = DefaultDataConverter.newDefaultInstance()
    .withPayloadConverterOverrides(
        new MyCustomPayloadConverter(),       // encoding: "json/my-custom"
        new JacksonJsonPayloadConverter(mapper) // replaces default Jackson converter
    );
```

## Protobuf Support

Protobuf messages are handled by `ProtobufJsonPayloadConverter` (enabled by default). It serializes `com.google.protobuf.Message` instances as JSON for human readability in the Temporal UI.

```java
// Protobuf messages work out of the box as workflow/activity params
@WorkflowInterface
public interface MyWorkflow {
    @WorkflowMethod
    MyProtoResult run(MyProtoInput input);
}
```

For binary protobuf encoding instead of JSON, use `ProtobufPayloadConverter`:

```java
DefaultDataConverter converter = DefaultDataConverter.newDefaultInstance()
    .withPayloadConverterOverrides(new ProtobufPayloadConverter());
```

## Payload Encryption

Use `PayloadCodec` with `CodecDataConverter` to encrypt/compress payloads:

```java
public class EncryptionCodec implements PayloadCodec {
    private final SecretKey key;

    public EncryptionCodec(SecretKey key) {
        this.key = key;
    }

    @Override
    public List<Payload> encode(List<Payload> payloads) {
        return payloads.stream().map(payload -> {
            // Encrypt payload.toByteArray() using your chosen algorithm (e.g., AES/GCM)
            byte[] encrypted = encryptBytes(payload.toByteArray(), key);
            return Payload.newBuilder()
                .putMetadata("encoding", ByteString.copyFromUtf8("binary/encrypted"))
                .setData(ByteString.copyFrom(encrypted))
                .build();
        }).collect(Collectors.toList());
    }

    @Override
    public List<Payload> decode(List<Payload> payloads) {
        return payloads.stream().map(payload -> {
            String encoding = payload.getMetadataOrDefault(
                "encoding", ByteString.EMPTY).toStringUtf8();
            if (!"binary/encrypted".equals(encoding)) return payload;
            // Decrypt and reconstruct the original Payload
            byte[] decrypted = decryptBytes(payload.getData().toByteArray(), key);
            return Payload.parseFrom(decrypted);
        }).collect(Collectors.toList());
    }
}
```

Apply the codec to the client:

```java
CodecDataConverter codecDataConverter = new CodecDataConverter(
    DefaultDataConverter.newDefaultInstance(),
    Collections.singletonList(new EncryptionCodec(secretKey))
);

WorkflowClient client = WorkflowClient.newInstance(
    service,
    WorkflowClientOptions.newBuilder()
        .setDataConverter(codecDataConverter)
        .build()
);
```

## Workflow Memo

Store arbitrary metadata with workflows (not searchable).

```java
// Set memo at workflow start
WorkflowOptions options = WorkflowOptions.newBuilder()
    .setWorkflowId("order-" + orderId)
    .setTaskQueue("orders")
    .setMemo(Map.of(
        "customer_name", order.getCustomerName(),
        "notes", "Priority customer"
    ))
    .build();
```

```java
// Read memo from workflow
@Override
public String run(Order order) {
    String notes = Workflow.getMemo("notes", String.class);
    // ...
}
```

## Deterministic APIs for Values

Use these APIs within workflows for deterministic values:

```java
@Override
public String run() {
    // Deterministic UUID (same on replay)
    String uniqueId = Workflow.randomUUID().toString();

    // Deterministic random (same on replay)
    Random rng = Workflow.newRandom();
    int value = rng.nextInt(100);

    // Deterministic current time (same on replay)
    long now = Workflow.currentTimeMillis();

    return uniqueId;
}
```

## Best Practices

1. Use Jackson `ObjectMapper` customization for complex serialization needs
2. Keep payloads small — see [Temporal common pitfalls](../core/gotchas.md) for limits
3. Encrypt sensitive data with `PayloadCodec` and `CodecDataConverter`
4. Use POJOs or Protobuf messages for workflow/activity parameters
5. Use `Workflow.randomUUID()`, `Workflow.newRandom()`, and `Workflow.currentTimeMillis()` for deterministic values

SHA-256: d1517a4dc499d2f274be59c9239385c48cceac586675a4a2b127061ae7199c89