Serializers
A serializer writes messages to a string when they're sent, and reads them back when they're received. Workflow state goes through it too. This page covers the default serializer, the message types it uses to restore your types, and writing your own serializer.
The problem with JSON
Messages are sent as JSON, which has no Dates or classes. Read back naively, a message is plain data:
const charged = new CreditCardCharged('tok_visa', 1200, new Date())
const received = JSON.parse(JSON.stringify(charged))
// Without message types, `chargedAt` is a string and `received` isn't a CreditCardCharged
console.log(typeof received.chargedAt) // 'string'The default serializer and message types
The default JsonSerializer restores Dates, Maps, Sets, bigints and class instances at any depth of a message or workflow state, using the message types generated by bus generate-message-types from @node-ts/bus-cli. The generator reads your TypeScript source, so messages stay plain classes, with no decorators or reflect-metadata.
npm i -D @node-ts/bus-cli typescript
npx bus generate-message-types --entry 'src/messages/**/*.ts'Pass the generated messageTypes to every bus that receives the messages:
const bus = Bus.configure()
// Restores the Dates, Maps, Sets, bigints and classes in received messages
.withMessageTypes(messageTypes)
.build()A bus that handles messages from several message libraries passes the message types of each, as in withMessageTypes(orderMessageTypes, messageTypes). They're merged when the bus is built, and build() throws MessageTypesConflict if two of them map the same $name to different types. At initialize(), a bus that receives messages throws MessageTypesMissing, naming what's missing, unless it has an entry for every message it handles and every workflow state it persists. Send-only buses don't need message types.
Messages stay plain JSON on the wire, so services that don't use the generated file, or aren't written in TypeScript, can still read them. Dates are sent as ISO strings, Maps as objects, Sets as arrays and bigints as strings.
The generator's options, the types it supports and its known limits are in the bus-cli README.
Constructors aren't run
Received messages and workflow state are created from their class' prototype, and their fields copied onto it. Constructor logic and field initializers don't run, so a field that's missing from the payload stays undefined. #private fields aren't sent or restored.
Custom serializers
A serializer implements Serializer from @node-ts/bus-core. The bus passes its message types to deserialize and toClass, so one serializer can be shared by several buses. Register it with withSerializer():
/**
* Writes messages as JSON, like the default serializer. A starting point for
* a serializer that, say, encrypts some fields.
*/
export class CustomSerializer implements Serializer {
private readonly json = new JsonSerializer()
serialize<T extends object>(obj: T): string {
return this.json.serialize(obj)
}
deserialize<T extends object>(
val: string,
classType: ClassConstructor<T>,
// The message types of the bus that's reading the message
messageTypes?: MessageTypes
): T {
return this.json.deserialize(val, classType, messageTypes)
}
toPlain<T extends object>(obj: T): object {
return this.json.toPlain(obj)
}
toClass<T extends object>(
obj: object,
classConstructor: ClassConstructor<T>,
messageTypes?: MessageTypes
): T {
return this.json.toClass(obj, classConstructor, messageTypes)
}
}
Bus.configure()
.withMessageTypes(messageTypes)
.withSerializer(new CustomSerializer())See also
- Class serializer, which generated message types replaced in 2.0
SerializerandgenerateMessageTypesin the API reference