Skip to content

feat: generalize event serialization/deserialization with decorators - #10

Closed
tjschutte wants to merge 4 commits into
mainfrom
serialization-improvements
Closed

feat: generalize event serialization/deserialization with decorators#10
tjschutte wants to merge 4 commits into
mainfrom
serialization-improvements

Conversation

@tjschutte

Copy link
Copy Markdown
Contributor
  • Add EventRegistry with @registerEvent and @serializable decorators
  • Replace hard-coded event type mappings with automatic registration
  • Eliminate repetitive serialization/deserialization code per event type
  • Update ApplicationSubmitted and ApplicationEvaluated to use decorators

- Add EventRegistry with @registerEvent and @serializable decorators
- Replace hard-coded event type mappings with automatic registration
- Eliminate repetitive serialization/deserialization code per event type
- Update ApplicationSubmitted and ApplicationEvaluated to use decorators
@tjschutte
tjschutte requested a review from lazamar August 26, 2025 18:28
@tjschutte tjschutte added the enhancement New feature or request label Aug 26, 2025
@lazamar

lazamar commented Aug 28, 2025

Copy link
Copy Markdown
Contributor

The thing about this approach is that the compiler has no way to tell us when we get things incorrectly like the fields required for the event payload when calling registerEvent.

We want to leverage the type-checker's knowledge of the fields in each event from the event's declaration to make sure that we can never get this wrong.

One very important issue with this approach is that if the serialised payload has incorrect fields for whatever reason (e.g. a field is a string but the event constructor expected a number) this passes this value with the incorrect type to the event constructor. This breaks the type contract of the constructor and will cause undefined behaviour.

We want all of our functions to be able to specify a type for their parameters and be confident that they will never be given different types and will never have to check that the types are correct. This is the cornerstone of type safety.

So if a serialised value has an unexpected type, we want this to be a deserialisation error and the constructor should never be called.

Another relevant point is that we will want to have rich abstract types as part of events, but their serialised representation might be different from their runtime representation. For example, an Id would be a serialised as a string but would be represented as an instance of some Id class at run time. Therefore we need to allow for custom encoding and decoding of these Id values and any other type we want.

On the Ashraf repo, for example, we allow that and have a rich set of types with independent runtime representation.
We have a concise way to do it by inferring the type from its schema.

type Payment = s.Infer<typeof schema_Payment>

const schema_Payment = s.object({
    member_id: IdMember.schema,
    tier_id: IdTier.schema,
    amount: Money.schema,
    paid_at: UTC.schema,
})

@lazamar

lazamar commented Oct 15, 2025

Copy link
Copy Markdown
Contributor

I'm closing this as we have a type-directed encoding and decoding approach in place now.
It consists of:

  • A library to create encoders (Object -> JSON), decoders (JSON -> Object) and schemas (an encoder together with a decoder). These have validation and error-handling built-in.
  • To avoid duplication, types are inferred from their decoder or schema definition. (example)
  • Event definitions are standardized to include as static properties:
    • A schema for the event, which is the only argument to the class constructor. (example)
    • A string literal type name which is unique among all events. (example). Uniqueness is enforced at runtime, but during program start. This is used to identify the event once it is serialized.
  • A central place where all event schemas to be used are declared. (here)
  • Centralised event encoding and decoding through the hydrate and encode methods.

This gives us:

  • Guarantee that we will be able to decode an event that we encoded. The schema library's types guarantee that.
  • Automatic error-handling if we can't decode an event sent by Ambar.
  • Automatic validation of command and query requests.
  • Excellent IDE hints because everything has strict types.
  • Ability to encode and decode types with different runtime and serialised representations. (e.g. Id is serialised as a string bug is represented as an object with a value property of type string at runtime).
  • No duplication from having to define a type and then define how it is serialised or deserialised.
  • Guarantee that types match their schemas because they are inferred from the schemas.

With this in place we have a very strong barrier between our system and the outside world and we are guaranteeing that any data within the system conforms to our strict type expectations.

@lazamar lazamar closed this Oct 15, 2025
@tjschutte
tjschutte deleted the serialization-improvements branch November 19, 2025 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants