Skip to main content

Data conversion - TypeScript SDK

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 DefaultPayloadConverter that handles undefined, binary data, and JSON. You only need a custom Payload Converter when your application uses types that aren't natively JSON-serializable, such as BigInt, Date, or protobufs.

How the default converter works

The default converter is a CompositePayloadConverter that tries each converter in order until one handles the value:

export class DefaultPayloadConverter extends CompositePayloadConverter {
constructor() {
super(
new UndefinedPayloadConverter(),
new BinaryPayloadConverter(),
new JsonPayloadConverter(),
);
}
}

During serialization, the Data Converter tries each Payload Converter in sequence until one returns a non-null Payload.

Custom Payload Converters

To handle custom data types, implement the PayloadConverter interface:

interface PayloadConverter {
toPayload<T>(value: T): Payload;
fromPayload<T>(payload: Payload): T;
}

Then provide it to the Worker and Client through a CompositePayloadConverter:

export const payloadConverter = new CompositePayloadConverter(
new UndefinedPayloadConverter(),
new EjsonPayloadConverter(),
);
// Worker
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
taskQueue: 'ejson',
dataConverter: {
payloadConverterPath: require.resolve('./payload-converter'),
},
});
// Client
const client = new Client({
dataConverter: {
payloadConverterPath: require.resolve('./payload-converter'),
},
});

EJSON example

The samples-typescript/ejson sample creates a custom PayloadConverter using EJSON, which supports types like Date, RegExp, Infinity, and Uint8Array.

ejson/src/ejson-payload-converter.ts

import {
EncodingType,
METADATA_ENCODING_KEY,
Payload,
PayloadConverterWithEncoding,
PayloadConverterError,
} from '@temporalio/common';
import EJSON from 'ejson';
import { decode, encode } from '@temporalio/common/lib/encoding';

/**
* Converts between values and [EJSON](https://docs.meteor.com/api/ejson.html) Payloads.
*/
export class EjsonPayloadConverter implements PayloadConverterWithEncoding {
// Use 'json/plain' so that Payloads are displayed in the UI
public encodingType = 'json/plain' as EncodingType;

public toPayload(value: unknown): Payload | undefined {
if (value === undefined) return undefined;
let ejson;
try {
ejson = EJSON.stringify(value);
} catch (e) {
throw new UnsupportedEjsonTypeError(
`Can't run EJSON.stringify on this value: ${value}. Either convert it (or its properties) to EJSON-serializable values (see https://docs.meteor.com/api/ejson.html ), or create a custom data converter. EJSON.stringify error message: ${errorMessage(
e,
)}`,
e as Error,
);
}

return {
metadata: {
[METADATA_ENCODING_KEY]: encode('json/plain'),
// Include an additional metadata field to indicate that this is an EJSON payload
format: encode('extended'),
},
data: encode(ejson),
};
}

public fromPayload<T>(content: Payload): T {
return content.data ? EJSON.parse(decode(content.data)) : content.data;
}
}

export class UnsupportedEjsonTypeError extends PayloadConverterError {
public readonly name: string = 'UnsupportedJsonTypeError';

constructor(
message: string | undefined,
public readonly cause?: Error,
) {
super(message ?? undefined);
}
}

Protobufs

To serialize values as Protocol Buffers:

  • Use protobufjs.

  • Use runtime-loaded messages (not generated classes) and MessageClass.create (not new MessageClass()).

  • Generate json-module.js:

    pbjs -t json-module --workflow-id commonjs -o protos/json-module.js protos/*.proto
  • Patch json-module.js:

protobufs/protos/root.js

const { patchProtobufRoot } = require('@temporalio/common/lib/protobufs');
const unpatchedRoot = require('./json-module');
module.exports = patchProtobufRoot(unpatchedRoot);

protobufs/src/payload-converter.ts

import { DefaultPayloadConverterWithProtobufs } from '@temporalio/common/lib/protobufs';
import root from '../protos/root';

export const payloadConverter = new DefaultPayloadConverterWithProtobufs({ protobufRoot: root });

For binary-encoded protobufs (saves space but can't be viewed in the Web UI):

import { ProtobufBinaryPayloadConverter } from '@temporalio/common/lib/protobufs';
import root from '../protos/root';

export const payloadConverter = new ProtobufBinaryPayloadConverter(root);

For binary-encoded protobufs alongside other default types:

import {
BinaryPayloadConverter,
CompositePayloadConverter,
JsonPayloadConverter,
UndefinedPayloadConverter,
} from '@temporalio/common';
import { ProtobufBinaryPayloadConverter } from '@temporalio/common/lib/protobufs';
import root from '../protos/root';

export const payloadConverter = new CompositePayloadConverter(
new UndefinedPayloadConverter(),
new BinaryPayloadConverter(),
new ProtobufBinaryPayloadConverter(root),
new JsonPayloadConverter(),
);

Provide the converter to the Worker and Client:

protobufs/src/worker.ts

const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: 'protobufs',
dataConverter: { payloadConverterPath: require.resolve('./payload-converter') },
});

protobufs/src/client.ts

import { Client, Connection } from '@temporalio/client';
import { loadClientConnectConfig } from '@temporalio/envconfig';
import { v4 as uuid } from 'uuid';
import { foo, ProtoResult } from '../protos/root';
import { example } from './workflows';

async function run() {
const config = loadClientConnectConfig();
const connection = await Connection.connect(config.connectionOptions);
const client = new Client({
connection,
dataConverter: { payloadConverterPath: require.resolve('./payload-converter') },
});

const handle = await client.workflow.start(example, {
args: [foo.bar.ProtoInput.create({ name: 'Proto', age: 2 })],
// can't do:
// args: [new foo.bar.ProtoInput({ name: 'Proto', age: 2 })],
taskQueue: 'protobufs',
workflowId: 'my-business-id-' + uuid(),
});

console.log(`Started workflow ${handle.workflowId}`);

const result: ProtoResult = await handle.result();
console.log(result.toJSON());
}

Use protobufs in your Workflows and Activities:

protobufs/src/workflows.ts

import { proxyActivities } from '@temporalio/workflow';
import { foo, ProtoResult } from '../protos/root';
import type * as activities from './activities';

const { protoActivity } = proxyActivities<typeof activities>({
startToCloseTimeout: '1 minute',
});

export async function example(input: foo.bar.ProtoInput): Promise<ProtoResult> {
const result = await protoActivity(input);
return result;
}

protobufs/src/activities.ts

import { foo, ProtoResult } from '../protos/root';

export async function protoActivity(input: foo.bar.ProtoInput): Promise<ProtoResult> {
return ProtoResult.create({ sentence: `${input.name} is ${input.age} years old.` });
}