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(notnew MessageClass()). -
Generate
json-module.js:pbjs -t json-module --workflow-id commonjs -o protos/json-module.js protos/*.proto -
Patch
json-module.js:
const { patchProtobufRoot } = require('@temporalio/common/lib/protobufs');
const unpatchedRoot = require('./json-module');
module.exports = patchProtobufRoot(unpatchedRoot);
-
Generate
root.d.ts:pbjs -t static-module protos/*.proto | pbts -o protos/root.d.ts - -
Create a
DefaultPayloadConverterWithProtobufs:
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:
const worker = await Worker.create({
workflowsPath: require.resolve('./workflows'),
activities,
taskQueue: 'protobufs',
dataConverter: { payloadConverterPath: require.resolve('./payload-converter') },
});
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:
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;
}
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.` });
}