Skip to content

Event Serialization

By default events are serialized to a plain payload, and deserialized again, by JsonEventSerializer. It needs no decorators:

  • Serializing copies the event’s own fields into plain objects, recursively. A nested class instance, such as a value object, becomes a plain object: an Id is stored as { props: { value } }. A Date stays a Date, which the SQL stores write as an ISO string and MongoDB as a date. A Set becomes an array and a Map an object with a key per entry. Getters and toJSON are ignored. An event that refers back to itself (a circular reference) fails the append with an EventSerializationException before anything is written.
  • Deserializing calls the event’s constructor without arguments, so the defaults it assigns fill the fields that older payloads lack, and copies the payload’s fields onto the event. Nested objects stay plain objects: a value object or a Date read from a SQL store doesn’t come back as an instance of its class. The values that the MongoDB driver returns as instances of its classes, such as a Binary for a buffer or a Long for an integer beyond 2^53, are kept.

This is what the 3.x default, class-transformer, did for classes without decorators, so events stored by 3.x read back the same. That works for events that only contain plain values. When an event contains value objects or dates that should read back as instances of their class, create an event serializer that converts the event to a payload of plain values and back.

The serializer is a class that implements the IEventSerializer interface and is decorated with @EventSerializer(), which takes the event it serializes.

import {
Event,
EventSerializer,
type IEvent,
type IEventPayload,
type IEventSerializer,
} from '@ocoda/event-sourcing';
@Event('account-opened')
export class AccountOpenedEvent implements IEvent {
constructor(
public readonly accountId: AccountId,
public readonly openedOn: Date,
public readonly accountOwnerIds?: AccountOwnerId[],
) {}
}
@EventSerializer(AccountOpenedEvent)
export class AccountOpenedEventSerializer implements IEventSerializer<AccountOpenedEvent> {
serialize({ accountId, openedOn, accountOwnerIds }: AccountOpenedEvent): IEventPayload<AccountOpenedEvent> {
return {
accountId: accountId.value,
openedOn: openedOn.toISOString(),
accountOwnerIds: accountOwnerIds?.map((id) => id.value),
};
}
deserialize({ accountId, openedOn, accountOwnerIds }: IEventPayload<AccountOpenedEvent>): AccountOpenedEvent {
return new AccountOpenedEvent(
AccountId.from(accountId),
openedOn && new Date(openedOn),
accountOwnerIds?.map((id) => AccountOwnerId.from(id)),
);
}
}

The keys of the payload that serialize returns are the keys that deserialize receives, so read the same names back (accountId in the example above).

Register the serializer as a provider in one of your modules and it’s used for that event instead of the default one. Serializers are discovered when the application bootstraps, see Module configuration. The event itself still needs to be registered, either in EventSourcingModule.forRoot() or in a feature module with EventSourcingModule.forFeature().

Events that use class-transformer decorators, such as @Type(() => Money), need ClassTransformerEventSerializer, which serializes with class-transformer as the 3.x default did. Install class-transformer (^0.5.1), an optional peer dependency, and make it the default serializer:

import { EventSourcingModule } from '@ocoda/event-sourcing';
import { ClassTransformerEventSerializer } from '@ocoda/event-sourcing/class-transformer';
EventSourcingModule.forRoot({
events: [AccountOpenedEvent, FundsDepositedEvent],
defaultEventSerializer: ClassTransformerEventSerializer,
});

defaultEventSerializer is used for every event without a serializer of its own. To use class-transformer for some events only, give each of them a serializer that extends ClassTransformerEventSerializer, and register it as a provider:

import { EventSerializer } from '@ocoda/event-sourcing';
import { ClassTransformerEventSerializer } from '@ocoda/event-sourcing/class-transformer';
@EventSerializer(FundsDepositedEvent)
export class FundsDepositedEventSerializer extends ClassTransformerEventSerializer<FundsDepositedEvent> {
constructor() {
super(FundsDepositedEvent);
}
}