-
-
Notifications
You must be signed in to change notification settings - Fork 55
fix: correctly infer JSON-serialized types for responses with .toJSON() methods #226
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Introduced an Account class with methods for account management and JSON serialization. - Updated server routes to utilize the new Account class. - Enhanced type handling with JSONSerialized type for better serialization of response data across various types.
Fix ddd serialization types
WalkthroughIntroduces a new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Tip 📝 Customizable high-level summaries are now available!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/fetch/types.ts (1)
3-10: JSONSerialized integration in EdenFetch.Fn success branch is correct; consider aligning error payloadsThe change to
data: JSONSerialized<Awaited<Route['response'][200]>>in the success branch matches the runtime JSON serialization semantics (honors.toJSON(), convertsDatetostring, removes functions, recurses arrays/objects). This is a solid fix for the mismatch between instance types and JSON responses.However, the error branch still uses
MapError<Route['response']>whereMapErrormaps status →EdenFetchError<Status, T[Status]>with the un-serialized response type. That means errorvaluetypes can still expose instance shapes (including methods) even though the wire payload is JSON-serialized, and Treaty2’sTreatyResponsenow usesJSONSerializedfor error values.To make error typing consistent with the new success behavior and with Treaty2, consider updating
MapErrorinsrc/types.tsto wrapT[K]withJSONSerialized:- export type MapError<T extends Record<number, unknown>> = [ + export type MapError<T extends Record<number, unknown>> = [ { [K in keyof T]-?: K extends ErrorRange ? K : never }[keyof T] ] extends [infer A extends number] ? { - [K in A]: EdenFetchError<K, T[K]> + [K in A]: EdenFetchError<K, JSONSerialized<T[K]>> }[A] : falseThis would cascade to both EdenFetch and EdenTreaty error typings.
Also applies to: 27-86
src/types.ts (1)
18-26: JSONSerialized design matches JSON serialization semantics; MapError could also leverage itThe new
JSONSerialized<T>helper is well‑structured:
- Prioritizes
.toJSON()when present and recursively appliesJSONSerializedto its return type (including the “toJSONreturns a function” edge case).- Converts
Date→string.- Removes bare function types (
T extends Function ? never) and, in the object branch, strips function properties via key remapping.- Preserves special non‑JSON payload types (
AsyncGenerator,Generator,ReadableStream,File,Blob) so they can be handled by other mechanisms.- Recursively transforms arrays and plain objects.
This achieves the intended behavior for domain instances and common JSON payloads.
Given this helper exists, you can also tighten
MapErrorso thatEdenFetchError.valuereflects the JSON‑serialized error body instead of the raw route type, aligning errors with success responses and withTreatyResponseintreaty2:-export type MapError<T extends Record<number, unknown>> = [ +export type MapError<T extends Record<number, unknown>> = [ { [K in keyof T]-?: K extends ErrorRange ? K : never }[keyof T] ] extends [infer A extends number] ? { - [K in A]: EdenFetchError<K, T[K]> + [K in A]: EdenFetchError<K, JSONSerialized<T[K]>> }[A] : falseThis change would cascade into both EdenFetch and EdenTreaty error typings and fully enforce JSON‑serialized shapes across the board.
Also applies to: 92-103, 106-140
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
example/server.ts(1 hunks)src/fetch/types.ts(2 hunks)src/treaty/types.ts(3 hunks)src/treaty2/types.ts(4 hunks)src/types.ts(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/fetch/types.ts (2)
src/types.ts (6)
TreatyToPath(92-102)IsNever(42-42)IsUnknown(44-48)Prettify(88-90)JSONSerialized(118-140)MapError(18-26)src/errors.ts (1)
EdenFetchError(1-8)
src/treaty/types.ts (3)
src/treaty/index.ts (2)
EdenWS(59-147)EdenTreaty(7-7)src/types.ts (1)
JSONSerialized(118-140)src/errors.ts (1)
EdenFetchError(1-8)
src/types.ts (1)
src/errors.ts (1)
EdenFetchError(1-8)
src/treaty2/types.ts (1)
src/types.ts (5)
IsNever(42-42)Not(104-104)Prettify(88-90)MaybeEmptyObject(64-78)JSONSerialized(118-140)
🔇 Additional comments (1)
example/server.ts (1)
212-212: Server type alias export looks good
export type Server = typeof appis a straightforward, zero‑runtime way to expose the server type to consumers; no issues.
| import type { IsUnknown, IsNever, MapError, Prettify, JSONSerialized } from '../types' | ||
| import type { EdenFetchError } from '../errors' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Treaty HTTP success data now correctly reflects JSON‑serialized shape; ensure errors/rawData are aligned
-
The change to
data: JSONSerialized<...Awaited<ReturnedType>...>inEdenTreaty.Sign’s HTTP branch aligns Treaty’s success typing with EdenFetch and with the actual JSON on the wire (honoring.toJSON(), Date, function stripping, etc.). This is a welcome fix. -
As with EdenFetch, the error side still relies on
MapError<Response>, whoseEdenFetchError.valuecurrently uses the raw route response type. If you adopt theMapErrorchange suggested insrc/types.ts(wrappingT[K]inJSONSerialized), Treaty’s error typings will automatically become consistent with these new success semantics. -
The addition of
rawData: MessageEvent['data']toOnMessage<Data>is useful, but it requires the runtimeEdenWSimplementation (insrc/treaty/index.ts) to actually populaterawDatawhen adaptingMessageEvent(e.g., by passing both parseddataand the originalws.data). It’s worth double‑checking that implementation so type and runtime stay in sync.
Also applies to: 26-127, 131-143, 162-165
🏁 Script executed:
#!/bin/bash
# Verify that Treaty WS message handling sets `rawData` when constructing the event.
rg -n "parseMessageEvent" src/treaty -C5
rg -n "rawData" src/treaty -C5Length of output: 1661
I need to check the parseMessageEvent implementation to confirm whether rawData is being properly set at runtime.Let me search the repository directly for the parseMessageEvent implementation.
Hmm, I need to check the actual code. Let me revise:
Actually, let me just look at the file directly:
🏁 Script executed:
#!/bin/bash
# Find the parseMessageEvent function definition in parsingUtils
rg -n "parseMessageEvent" src/utils -A 10Length of output: 411
🏁 Script executed:
#!/bin/bash
# Find the parseMessageEvent function definition in parsingUtils
rg -n "function parseMessageEvent|export.*parseMessageEvent" src/utils -A 10Length of output: 411
🏁 Script executed:
#!/bin/bash
# Find parsingUtils file and show parseMessageEvent implementation
fd "parsingUtils" src/utils --exec cat {}Length of output: 2289
Runtime does not populate rawData field—MessageEvent.data is overwritten with parsed data
The type contract at src/treaty/types.ts:164 declares rawData: MessageEvent['data'] as required, but the implementation does not provide it:
- At
src/treaty/index.ts:118,parseMessageEvent(ws)returns only the parsed value (seesrc/utils/parsingUtils.ts:66-72) - The spread
{ ...ws, data }at lines 120–123 overwrites the originalws.datawith the parsed result - The original untouched message data is lost;
rawDatais never assigned
This breaks the type contract—calling code cannot safely access onMessage.rawData (it will be undefined). Either remove rawData from the type or update the runtime at src/treaty/index.ts:118–123 to preserve the original data:
const data = parseMessageEvent(ws as MessageEvent)
listener({
...ws,
data,
rawData: ws.data
} as any)
| import type { IsNever, MaybeEmptyObject, Not, Prettify, JSONSerialized } from '../types' | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
TreatyResponse now models JSON‑serialized success and error bodies; confirm WS rawData wiring
-
The updated
TreatyResponse<Res>correctly wraps both successdataand errorvalueinJSONSerialized<...>, while still respecting theELYSIA_FORM_DATAescape hatch and theReplaceGeneratorWithAsyncGeneratorpreprocessing. This gives a much more accurate view of what clients actually receive, especially for domain instances with.toJSON()and forDatefields. -
JSONSerialized’s special‑casing of
AsyncGenerator,ReadableStream,File, andBlobfits well with how these types are typically handled outside JSON, so the combination withReplaceGeneratorWithAsyncGeneratoris sound. -
The new
rawData: MessageEvent['data']onOnMessage<Data>mirrors the change insrc/treaty/types.ts; just ensure the treaty2 WebSocket implementation populatesrawDataalongside the parseddatawhen adaptingMessageEvent, so consumer code doesn’t see an always‑undefined field.
Also applies to: 21-41, 66-72, 73-154, 175-193, 199-235, 237-240
🏁 Script executed:
#!/bin/bash
# Verify that Treaty2 WS message handling sets `rawData` appropriately.
rg -n "OnMessage<" src/treaty2 -C3
rg -n "rawData" src/treaty2 -C5
rg -n "MessageEvent" src/treaty2 -C5Length of output: 3025
Now I need to check the actual implementation of the parseMessageEvent function to confirm if rawData is being handled correctly.
---
🏁 Script executed:
```shell
#!/bin/bash
# Find and examine parseMessageEvent implementation
fd -t f "parsingUtils" src/
cat src/utils/parsingUtils.ts 2>/dev/null || cat src/utils/parsingUtils.js 2>/dev/null || echo "File not found"
Length of output: 106
---
🏁 Script executed:
```shell
#!/bin/bash
# Find and examine parseMessageEvent implementation
fd -t f "parsingUtils" . --type f
find . -name "*parsingUtils*" -type f 2>/dev/null
Length of output: 55
---
🏁 Script executed:
```shell
#!/bin/bash
# Examine parseMessageEvent implementation
cat ./src/utils/parsingUtils.ts
Length of output: 2289
rawData field in OnMessage is typed but not populated in EdenWS listener; consumer code will see undefined
The type definition declares rawData: MessageEvent['data'] in OnMessage<Data> (src/treaty2/types.ts:239), but the EdenWS listener (src/treaty2/ws.ts:62-67) never captures or assigns it. The parseMessageEvent function transforms event.data into data, and while the original spread includes native MessageEvent properties, rawData is a custom field that must be explicitly set. The listener should capture the original unparsed data before overriding it:
const data = parseMessageEvent(ws as MessageEvent)
listener({
...ws,
data,
rawData: ws.data // ← missing assignment
} as any)Without this, consumer code accessing rawData receives undefined instead of the original message string.
🤖 Prompt for AI Agents
src/treaty2/types.ts around lines 5-6: the OnMessage type declares rawData but
the EdenWS listener in src/treaty2/ws.ts (around lines 62-67) never assigns it,
so consumers see undefined; fix by capturing the original MessageEvent.data
before parsing and include it as rawData when calling the listener (e.g., let
original = ws.data; const data = parseMessageEvent(ws as MessageEvent);
listener({ ...ws, data, rawData: original } as any)), ensuring rawData holds the
unparsed message string.
Problem
Eden's type inference was showing instance types instead of their JSON-serialized equivalents when Elysia routes return objects that implement
.toJSON()methods. This is kinda sucks when working w DDD (Domain-Driven Design) patterns where domain objects are serialized before sending to clients.Example of the bug:
this creates a mismatch where TypeScript shows methods like .findUser()
Root Cause
Eden was using Awaited<Route['response'][200]> directly without accounting for the fact that Elysia calls JSON.stringify() on all responses, which:
automatically invokes .toJSON() on objects that implement it
Converts Date objects to strings
Removes functions from objects
Solution
Added a JSONSerialized type utility that mirrors TypeScript's understanding of JSON serialization:
Breaking Changes
None.
Summary by CodeRabbit
rawDataproperty to message event handlers for direct access to underlying message data