Skip to content

Conversation

@aymaneallaoui
Copy link

@aymaneallaoui aymaneallaoui commented Nov 16, 2025

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:

// Elysia server
class AccountInstance {
  constructor(public id: string, public name: string) {}
  
  findUser() { /* domain method */ }
  deleteAccount() { /* domain method */ }
  
  toJSON() {
    return { id: this.id, name: this.name }
  }
}

app.get('/account', () => {
  return [new AccountInstance('1', 'John')]
})

// Eden client
const { data } = api.account.get()
// Before fix: data has type AccountInstance[] 
// Runtime: data is actually { id: string, name: string }[] 

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:

  • Detects and uses return types of .toJSON() methods
  • Converts Date → string
  • Recursively handles arrays and nested objects
  • Removes function properties
  • Preserves special types (AsyncGenerator, Generator, ReadableStream, File, Blob)

Breaking Changes
None.

Summary by CodeRabbit

  • Type System Enhancements
    • Response data types now accurately represent JSON-serialized formats, ensuring type safety for actual data structures received
    • Added rawData property to message event handlers for direct access to underlying message data
    • Improved type precision across fetch and treaty APIs

aymaneallaoui and others added 4 commits November 15, 2025 15:27
- 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.
@coderabbitai
Copy link

coderabbitai bot commented Nov 16, 2025

Walkthrough

Introduces a new JSONSerialized type helper and applies it across fetch, treaty, and treaty2 modules to standardize how JSON-serialized data types are represented in public APIs. Minor formatting adjustments included.

Changes

Cohort / File(s) Summary
New Type Definition
src/types.ts
Adds new exported JSONSerialized<T> type alias that models JSON.stringify() serialization semantics, handling toJSON() methods, Date→string conversion, function exclusion, and recursive serialization for arrays/objects.
Type Integration Across Modules
src/fetch/types.ts, src/treaty/types.ts, src/treaty2/types.ts
Imports and applies JSONSerialized wrapper to response data types: EdenFetch.Fn.data now uses JSONSerialized<Awaited<Route['response'][200]>>; TreatyResponse.data and error values wrapped with JSONSerialized; OnMessage interface extended with rawData: MessageEvent['data'] property.
Formatting Adjustment
example/server.ts
Minor formatting/no-newline-at-end-of-file adjustment to exported Server type alias.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify JSONSerialized type semantics correctly model JSON serialization across different type hierarchies (Date, functions, generators, etc.)
  • Cross-check consistent application of wrapper in success/error branches across three different modules (fetch, treaty, treaty2)
  • Confirm OnMessage.rawData addition in treaty2 doesn't conflict with existing message handling patterns
  • Validate that the type changes don't break downstream consumers of these public APIs

Poem

🐰 A new serialized friend joins the fold,
JSON shapes wrapped in types of gold,
From fetch to treaty, the pattern flows,
Data transformed where knowledge goes! ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: introducing a JSONSerialized type utility to correctly infer JSON-serialized types for responses with .toJSON() methods, addressing the core problem across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

  • Provide custom instructions to shape the summary (bullet lists, tables, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example:

"Create a concise high-level summary as a bullet-point list. Then include a Markdown table showing lines added and removed by each contributing author."


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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 payloads

The change to data: JSONSerialized<Awaited<Route['response'][200]>> in the success branch matches the runtime JSON serialization semantics (honors .toJSON(), converts Date to string, 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']> where MapError maps status → EdenFetchError<Status, T[Status]> with the un-serialized response type. That means error value types can still expose instance shapes (including methods) even though the wire payload is JSON-serialized, and Treaty2’s TreatyResponse now uses JSONSerialized for error values.

To make error typing consistent with the new success behavior and with Treaty2, consider updating MapError in src/types.ts to wrap T[K] with JSONSerialized:

- 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]
     : false

This 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 it

The new JSONSerialized<T> helper is well‑structured:

  • Prioritizes .toJSON() when present and recursively applies JSONSerialized to its return type (including the “toJSON returns a function” edge case).
  • Converts Datestring.
  • 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 MapError so that EdenFetchError.value reflects the JSON‑serialized error body instead of the raw route type, aligning errors with success responses and with TreatyResponse in treaty2:

-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]
     : false

This 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

📥 Commits

Reviewing files that changed from the base of the PR and between d5441e9 and 8f9734c.

📒 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 app is a straightforward, zero‑runtime way to expose the server type to consumers; no issues.

Comment on lines +4 to 5
import type { IsUnknown, IsNever, MapError, Prettify, JSONSerialized } from '../types'
import type { EdenFetchError } from '../errors'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Treaty HTTP success data now correctly reflects JSON‑serialized shape; ensure errors/rawData are aligned

  • The change to data: JSONSerialized<...Awaited<ReturnedType>...> in EdenTreaty.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>, whose EdenFetchError.value currently uses the raw route response type. If you adopt the MapError change suggested in src/types.ts (wrapping T[K] in JSONSerialized), Treaty’s error typings will automatically become consistent with these new success semantics.

  • The addition of rawData: MessageEvent['data'] to OnMessage<Data> is useful, but it requires the runtime EdenWS implementation (in src/treaty/index.ts) to actually populate rawData when adapting MessageEvent (e.g., by passing both parsed data and the original ws.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 -C5

Length 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 10

Length of output: 411


🏁 Script executed:

#!/bin/bash
# Find the parseMessageEvent function definition in parsingUtils
rg -n "function parseMessageEvent|export.*parseMessageEvent" src/utils -A 10

Length 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 (see src/utils/parsingUtils.ts:66-72)
  • The spread { ...ws, data } at lines 120–123 overwrites the original ws.data with the parsed result
  • The original untouched message data is lost; rawData is 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)

Comment on lines +5 to 6
import type { IsNever, MaybeEmptyObject, Not, Prettify, JSONSerialized } from '../types'

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

TreatyResponse now models JSON‑serialized success and error bodies; confirm WS rawData wiring

  • The updated TreatyResponse<Res> correctly wraps both success data and error value in JSONSerialized<...>, while still respecting the ELYSIA_FORM_DATA escape hatch and the ReplaceGeneratorWithAsyncGenerator preprocessing. This gives a much more accurate view of what clients actually receive, especially for domain instances with .toJSON() and for Date fields.

  • JSONSerialized’s special‑casing of AsyncGenerator, ReadableStream, File, and Blob fits well with how these types are typically handled outside JSON, so the combination with ReplaceGeneratorWithAsyncGenerator is sound.

  • The new rawData: MessageEvent['data'] on OnMessage<Data> mirrors the change in src/treaty/types.ts; just ensure the treaty2 WebSocket implementation populates rawData alongside the parsed data when adapting MessageEvent, 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 -C5

Length 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant