Skip to content

fix: fromTypes response parsing — numeric keys, type alias inlining, import() resolution#329

Open
alexng353 wants to merge 10 commits into
elysiajs:mainfrom
alexng353:fix/fromtypes-response-parsing
Open

fix: fromTypes response parsing — numeric keys, type alias inlining, import() resolution#329
alexng353 wants to merge 10 commits into
elysiajs:mainfrom
alexng353:fix/fromtypes-response-parsing

Conversation

@alexng353

@alexng353 alexng353 commented Feb 23, 2026

Copy link
Copy Markdown

Summary

fromTypes() fails to generate correct OpenAPI response schemas in several common scenarios. This PR fixes three parsing bugs and adds support for cross-module type resolution:

  • Numeric key regex — The regex (\d+):/g that quotes status code keys (e.g. 200: -> "200":) also matches digits inside identifiers like v4:, producing v"4": which TypeBox rejects. Fixed with a lookbehind: /(?<=^|[{;,\s])(\d+):/g.

  • Type alias inlining — When TypeScript emits type aliases in .d.ts (e.g. type User = { id: string; ... }) and references them in route responses, TypeBox produces unresolvable $ref nodes. Fixed by extracting type aliases from the declaration and inlining them before TypeBox parsing.

  • Route section trimmingextractRootObjects could include trailing content from adjacent type declarations. Fixed with brace-depth scanning to extract only the route object.

  • Cross-module import() resolution — When tsc emits import("@some/package").TypeName references (common with Drizzle ORM and other codegen), these are not parseable by TypeBox. Fixed by using TypeScript's ts.resolveModuleName() to locate the source file, extract the type definition, and inline it. This handles monorepo workspace packages, path aliases, and exports maps correctly.

  • Build config — Added typescript to external in tsup config to prevent bundling it (was causing ~9.7MB output).

Bugs this fixes

Given an Elysia app like:

type User = { id: string; name: string; email: string; }

const app = new Elysia()
  .post("/api/v4/getUser", ({ body }) => {
    return user as User
  }, { body: t.Object({ id: t.String() }) })
  .use(openapi({ references: fromTypes("app.ts") }))

Before: The OpenAPI spec either has no responses at all, or produces {"not":{}} schemas due to the numeric key corruption.

After: Full response schemas are generated automatically:

{
  "responses": {
    "200": {
      "content": {
        "application/json": {
          "schema": {
            "type": "object",
            "required": ["id", "name", "email"],
            "properties": {
              "id": { "type": "string" },
              "name": { "type": "string" },
              "email": { "type": "string" }
            }
          }
        }
      }
    }
  }
}

Test plan

  • 24 new unit tests covering all fixed behaviors (numeric key regex, type alias extraction/inlining, route trimming, import() stripping, complex Drizzle-derived types)
  • Verified end-to-end against a real monorepo with Drizzle ORM types crossing module boundaries (workspace packages with exports maps)
  • Existing tests unaffected (the integrate test still fails due to tsc not on PATH in CI — pre-existing issue)

This code was written by Claude (Opus 4.6) and reviewed by @alexng353.

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

Summary by CodeRabbit

  • Chores
    • Mark TypeScript and @sinclair/typebox as external during bundling.
    • Add TypeScript (>= 5.0.0) and @sinclair/typebox (>= 0.34.0 < 1) as peer dependencies in the package manifest.
    • Bump dev dependency "elysia" from 1.4.19 to 1.4.25.

alexng353 and others added 6 commits February 22, 2026 19:23
…ing, route section trimming

Three bugs prevented fromTypes from correctly parsing auto-generated
.d.ts declarations into OpenAPI response schemas:

1. numberKey regex matched digits inside identifiers (e.g. v4: → v"4":)
   causing TypeBox to produce invalid schemas. Fixed with lookbehind
   to only match standalone numeric keys.

2. Type alias references (e.g. User) produced unresolvable $refs since
   TypeBox has no type definitions context. Added extractTypeAliases()
   and inlineTypeReferences() to substitute type bodies before parsing.

3. Trailing generic params after the routes map were included in
   extraction, producing many spurious objects. Added brace-depth
   scanning to trim at the routes param boundary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… trimming

Tests cover all three bug fixes:

- numberKey regex: digits inside identifiers (v4) are preserved while
  standalone numeric keys (200, 404) are still quoted
- extractTypeAliases / inlineTypeReferences: type alias extraction from
  declaration preambles and substitution into route schemas
- declarationToJSONSchema with typeAliases param: end-to-end type alias
  inlining producing correct JSON Schema for User, ErrorResponse, etc.
- Route section trimming: extractRootObjects returns correct count

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TypeBox cannot resolve cross-module import("...").TypeName references,
producing invalid schemas. These now get replaced with `unknown` before
parsing, which maps to `{}` in JSON Schema (any value).

Tests added for import() stripping and a full Drizzle-derived User type
with nullable fields, string literal unions, optional nested objects,
and array types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…odule resolution

Instead of replacing import("...").TypeName with `unknown`, the plugin
now resolves the import path using ts.resolveModuleName() (which handles
tsconfig paths, package.json exports, workspace packages, etc.), reads
the source file, extracts the type alias, and inlines it.

This means Drizzle ORM types and other cross-package types get full
JSON Schema output instead of degrading to `{}`.

Also:
- Strip comments from extracted type alias bodies (// and /* */)
- Externalize typescript from the bundle to avoid 9MB dist
- Use source file path as resolution context for workspace symlinks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lback

resolveImportedTypes now throws a clear error if the typescript package
cannot be loaded, rather than silently falling back to manual path
heuristics. TypeScript's module resolution is the only reliable way to
resolve cross-module import() references in monorepos and workspace
packages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 23, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 77d7c22 and 973db1e.

⛔ Files ignored due to path filters (3)
  • bun.lock is excluded by !**/*.lock
  • src/gen/index.ts is excluded by !**/gen/**
  • test/gen/index.test.ts is excluded by !**/gen/**
📒 Files selected for processing (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

Walkthrough

Build config and manifest updated: build.ts marks typescript and @sinclair/typebox as externals for bundling; package.json updates elysia dev dependency and adds peerDependencies entries for typescript (>=5.0.0) and @sinclair/typebox (>=0.34.0 <1).

Changes

Cohort / File(s) Summary
Build Configuration
build.ts
Added external: ['typescript', '@sinclair/typebox'] to tsupConfig, keeping bundle: true.
Package Manifest
package.json
Updated devDependency elysia from 1.4.191.4.25; added peerDependencies typescript: ">= 5.0.0" and @sinclair/typebox: ">= 0.34.0 < 1".

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 I twitch my whiskers, code in paw,
Two types step out and lighten the draw.
Bundles skip what they need to spare,
Deps announced with a quiet flair.
A tiny hop, a satisfied stare.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly addresses the main bugs being fixed in the changeset: numeric key parsing in fromTypes(), type alias inlining, and import() resolution for cross-module types.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@build.ts`:
- Around line 11-12: The build config marks typescript as external (external:
['typescript']) so it isn't bundled, but runtime code uses
ts.resolveModuleName() via import() so consumers must provide TypeScript; update
package.json to declare "typescript" as a peerDependency (or move to
dependencies if you intend to bundle/own it) and bump the package.json version
if required, ensuring the package.json fields "peerDependencies" (add
"typescript": "<minimum-supported-version>") reflect the same runtime
requirement referenced by external in build.ts and the ts.resolveModuleName()
usage.

Comment thread build.ts Outdated
typescript is externalized in the build (not bundled) but required at
runtime by resolveImportedTypes() via require('typescript'). Declaring
it as a peerDependency ensures package managers surface a warning if
it's missing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
package.json (1)

89-92: typescript peer dependency should be marked optional via peerDependenciesMeta.

typescript is only needed at runtime for fromTypes() / resolveImportedTypes() — users who don't call that feature have no need for it. As a non-optional peerDependency, every consumer of this package will receive peer-dep warnings (npm) or hard errors (pnpm) if they don't have TypeScript installed, even though it's entirely irrelevant to their usage.

Marking a peer dependency as optional ensures npm will not emit a warning if the package is not installed on the host. With Bun, running bun install will install peer dependencies by default, unless marked optional in peerDependenciesMeta.

🛠️ Proposed fix
     "peerDependencies": {
         "elysia": ">= 1.4.0",
         "typescript": ">= 5.0.0"
     },
+    "peerDependenciesMeta": {
+        "typescript": {
+            "optional": true
+        }
+    },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 89 - 92, The package declares "typescript" under
"peerDependencies" causing warnings/errors for consumers who don't need it; add
a "peerDependenciesMeta" entry marking "typescript" as optional so package
managers won't warn/install it by default. Update package.json to include a
peerDependenciesMeta object with "typescript": { "optional": true } alongside
the existing "peerDependencies" to make the typescript peer dep optional (this
targets the "peerDependencies" and "typescript" symbols in package.json).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Around line 93-95: The package currently lists "@sinclair/typebox" in
"dependencies" which causes its types (e.g., TProperties used in the exported
withHeaders function) to be embedded in the public API and can cause
duplicate-type conflicts for consumers; move "@sinclair/typebox" from
"dependencies" to "peerDependencies" and update its version string to a
patch-pinned form "~0.34.0" so consumers provide a compatible TypeBox instance
and TypeScript resolution remains consistent (ensure package.json entries for
"dependencies" and "peerDependencies" are updated accordingly and keep
withHeaders/TProperties usage unchanged).

---

Nitpick comments:
In `@package.json`:
- Around line 89-92: The package declares "typescript" under "peerDependencies"
causing warnings/errors for consumers who don't need it; add a
"peerDependenciesMeta" entry marking "typescript" as optional so package
managers won't warn/install it by default. Update package.json to include a
peerDependenciesMeta object with "typescript": { "optional": true } alongside
the existing "peerDependencies" to make the typescript peer dep optional (this
targets the "peerDependencies" and "typescript" symbols in package.json).

Comment thread package.json Outdated
typebox types (TProperties, TObject) are exposed in the public API via
the exported withHeaders function. Having typebox as a hard dependency
can cause duplicate instances when the consumer's elysia (which also
peer-depends on typebox) resolves a different copy, leading to type
incompatibilities. Moving to peerDependencies ensures a single shared
instance, matching elysia's own approach.

Also added @sinclair/typebox to the tsup external list so it is not
bundled.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
package.json (1)

92-92: Mark typescript as optional in peerDependenciesMeta.

typescript is only required by consumers who use the fromTypes() API. Basic Swagger/OpenAPI users don't need TypeScript as a runtime peer. Without peerDependenciesMeta, package managers will warn all consumers about a missing typescript peer, even those who never invoke fromTypes().

♻️ Proposed change
     "peerDependencies": {
         "@sinclair/typebox": ">= 0.34.0 < 1",
         "elysia": ">= 1.4.0",
         "typescript": ">= 5.0.0"
     },
+    "peerDependenciesMeta": {
+        "typescript": {
+            "optional": true
+        }
+    },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 92, Add a peerDependenciesMeta entry to package.json to
mark TypeScript as optional so package managers won't warn consumers who don't
need it; specifically, under the existing peerDependencies/ "typescript": ">=
5.0.0" ensure there is a peerDependenciesMeta object with "typescript": {
"optional": true } (preserving the same version spec in peerDependencies) so
only consumers using the fromTypes() API need to install TypeScript.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@package.json`:
- Line 92: Add a peerDependenciesMeta entry to package.json to mark TypeScript
as optional so package managers won't warn consumers who don't need it;
specifically, under the existing peerDependencies/ "typescript": ">= 5.0.0"
ensure there is a peerDependenciesMeta object with "typescript": { "optional":
true } (preserving the same version spec in peerDependencies) so only consumers
using the fromTypes() API need to install TypeScript.

alexng353 and others added 2 commits February 23, 2026 02:17
- Add flattenNestedIntersections() to distribute parent structure over
  inner `& {}` boundaries from multi-route Elysia plugins
- Add extractGenericParam() for brace-aware comma parsing of Elysia's
  generic parameters (replaces fragile `}, {` slicing)
- Bump elysia devDep to 1.4.25
- Update test expectations for cleaner route output (no derive/resolve
  noise)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@SaltyAom

SaltyAom commented Mar 31, 2026

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

1 similar comment
@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@alexng353

Copy link
Copy Markdown
Author

Hello @SaltyAom , just a heads up, I've been using my fork of elysia-openapi in production since this PR was opened. I've not experienced any bugs myself. If you need any information regarding this PR, I'm afraid I will not be able to answer before reviewing the code again myself, as I haven't taken a look at this in over a month.

Please be patient with me for any inquiries you have, work is extremely heavy for me at the moment, but I will get to anything you need as soon as possible.

@0-don

0-don commented Apr 24, 2026

Copy link
Copy Markdown

yes please, have been waiting for this! @SaltyAom

@0-don

0-don commented Apr 24, 2026

Copy link
Copy Markdown

@alexng353 can you update to latest version from upstream

@0-don

0-don commented Apr 24, 2026

Copy link
Copy Markdown

Used this branch as base for a downstream fork: https://github.com/0-don/elysia-openapi-fork/tree/fix/fromtypes-response-parsing-dist (review commits on src/; dist/ committed for direct bun install)

@Kapsonfire

Copy link
Copy Markdown

I was able to inline my refs myself via a util helper

    export interface Test {
    name: string;
    age: number;
}


  .get('/test/omfg3', () : Prettify<Test> => {

        return {
            name: 'Hello',
            age: 4
        } as Test;
    })
    ```
    
    

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.

4 participants