Skip to content

Conversation

@MarcelOlsen
Copy link
Contributor

@MarcelOlsen MarcelOlsen commented Nov 11, 2025

This resolves #1490
(This has been previously worked on in #1533, but I've messed up my local git instance)

Problem

Guard schemas defined via guard() are not accessible to plugins like OpenAPI. Routes appear in documentation but lack request body and parameter schemas.

Current Behavior

app.guard({
  body: t.Object({
    username: t.String(),
    password: t.String()
  })
}, (app) =>
  app
    .post('/sign-up', ({ body }) => body)
    .post('/sign-in', ({ body }) => body)
)
  • Runtime: Works correctly (validation, types)
  • OpenAPI: Routes exist but have no requestBody schema

Root Cause

Guard schemas are stored in route.hooks.standaloneValidator[] arrays. Plugins only read direct schema properties like route.hooks.body.

Solution

Add getFlattenedRoutes() method that merges standaloneValidator arrays into direct hook properties.

Changes

New Methods:

  • getFlattenedRoutes(): Returns routes with merged schemas (protected)
  • mergeStandaloneValidators(): Flattens standaloneValidator arrays
  • mergeSchemaProperty(): Handles TypeBox schema merging
  • mergeResponseSchema(): Handles response status codes

Tests:

  • test/core/flattened-routes.test.ts: 7 tests covering all schema types and edge cases

Backward Compatibility

  • getGlobalRoutes() unchanged - existing behavior preserved
  • Zero breaking changes - purely additive
  • Plugins can use app.getFlattenedRoutes?.() ?? app.getGlobalRoutes() for compatibility

Usage

Plugins can now access guard schemas:

// Before: standaloneValidator only
const routes = app.getGlobalRoutes()
// routes[0].hooks.body === undefined
// routes[0].hooks.standaloneValidator === [{ body: {...} }]

// After: schemas merged
const routes = app.getFlattenedRoutes()
// routes[0].hooks.body === { type: 'object', properties: {...} }
// routes[0].hooks.standaloneValidator === [{ body: {...} }] (preserved)

Demo

Reproduction case and full documentation: https://github.com/MarcelOlsen/elysia-guard-openapi-fix-demo

Related PR

OpenAPI plugin PR: elysiajs/elysia-openapi

Testing

bun test test/core/flattened-routes.test.ts

All 7 tests pass.

Summary by CodeRabbit

  • Refactor

    • Enhanced route schema merging so guard-provided validations (body, headers, query, params, cookie, response) are folded into route-level validators; preserves guard status codes while allowing route-specific 200 overrides and normalizes string references.
  • Behavior

    • Error handling now respects custom error-provided responses and maps their status/headers, ensuring custom toResponse-style responses are returned consistently.
  • Tests

    • Added comprehensive tests for flattened routes, schema merging, reference handling, response-status preservation, and custom error response handling.

   plugins

   - Add protected getFlattenedRoutes() method that merges
   standaloneValidator schemas into direct hook properties
   - This allows plugins to access schemas defined in guard() blocks
   - Implement mergeStandaloneValidators(), mergeSchemaProperty(), and
   mergeResponseSchema() helper methods
   - Handle complex schema merging including status code response objects
   - Add comprehensive test suite covering various guard schema scenarios
@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

Caution

Review failed

Failed to post review comments

Walkthrough

Adds internals to merge guard-provided standalone validator schemas into per-route hook schemas, introduces protected route-flattening, adjusts error handling to honor error.toResponse(), and adds tests for flattened routes and toResponse-driven error flows. No public API signature changes.

Changes

Cohort / File(s) Summary
Route flattening & schema merge helpers
src/index.ts
Import mergeObjectSchemas; added protected getFlattenedRoutes() and private helpers mergeStandaloneValidators(hooks), mergeSchemaProperty(...), mergeResponseSchema(...), normalizeSchemaReference(), and isTSchema() to fold guard standaloneValidator entries into route hook schemas (body, headers, query, params, cookie, response).
Flattened routes tests
test/core/flattened-routes.test.ts
New test suite validating getFlattenedRoutes() behavior: propagation and merging of guard-provided standalone validators into route hooks, string refs/TRef handling, response status-map merging and precedence, nested guards, and preservation of standaloneValidator arrays.
Error-to-response handling (Bun adapter)
src/adapter/bun/handler.ts
Expanded errorToResponse implementation to short-circuit to error.toResponse() when present (mapped via mapResponse with default set), otherwise preserve previous JSON error payload/status behavior.
Error-to-response handling (Web Standard adapter)
src/adapter/web-standard/handler.ts
Mirror change: errorToResponse now delegates to error.toResponse() when available, else falls back to original JSON error payload logic.
Runtime error flow injection
src/compose.ts
Adds early-return handling in the error path: when error.toResponse() exists (and error is not validation/transform decode), call it, apply status if Response, run afterResponse, and return mapped response via mapResponse before existing onError logic.
Tests for toResponse error handling
test/core/handle-error.test.ts
New tests covering returned/thrown errors and non-Error values exposing toResponse(), asserting status, body, and custom headers are preserved.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant App as Elysia App
    participant Flattener as getFlattenedRoutes()
    participant Guard as Guard Layer(s)
    participant Merger as mergeStandaloneValidators()
    participant Output as FlattenedRoute

    Note right of App: used for metadata export / tests
    App->>Flattener: collect routes (with guards & hooks)
    Flattener->>Guard: read standaloneValidator(s)
    Flattener->>Merger: provide route.hooks + standaloneValidator
    Merger->>Merger: merge properties (body, headers, query, params, cookie)
    Merger->>Merger: mergeResponseSchema for response status maps
    Merger-->>Flattener: merged hook schemas
    Flattener-->>Output: return flattened route with combined hooks
Loading
sequenceDiagram
    autonumber
    participant Request as Incoming Request
    participant Router as Router/Handler
    participant Error as Thrown/Returned Error
    participant toResp as error.toResponse()
    participant Map as mapResponse()
    participant After as afterResponse()
    participant Final as Final Response

    Request->>Router: handle -> throws/returns Error
    Router->>Error: detect Error
    alt error has toResponse() && not validation/transform errors
        Router->>toResp: call toResponse()
        toResp->>Map: produce mapped response
        Map->>After: run afterResponse()
        After->>Final: return mapped response (early)
    else
        Router->>Router: existing onError / fallback JSON error payload
        Router->>Final: return fallback response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Areas needing extra attention:
    • mergeResponseSchema status-code precedence, union/intersect handling, and string-ref normalization in src/index.ts.
    • Interaction between nested guard merges and route-level schemas (ordering and precedence).
    • Early-return error flow in src/compose.ts and its interaction with existing onError handlers and afterResponse.
    • Adapter implementations in src/adapter/*/handler.ts for consistent behavior and default set values.
    • New tests validity and edge-case coverage in test/core/*.

Possibly related PRs

  • 1.4.5 patch #1403 — Implements runtime flattening and merging of guard-provided standalone validators into per-route hook properties; closely related to the same merge pathway.
  • Elysia 1.4: Weirs #1398 — Modifies the standalone-validator / schema-merging pathway and introduces utilities overlapping with these changes.

Poem

🐰 I hopped through guards both snug and deep,

Folded schemas where the shadows keep,
Routes now wear each guardian's thread,
Errors speak back the shape they said,
— a rabbit with a tiny dev's head 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive The PR includes changes to error handling (compose.ts, handler files) via toResponse() that appear unrelated to the guard schema exposure objective, creating uncertainty about scope alignment. Clarify the relationship between error handling changes and the guard schema exposure feature, or consider separating them into distinct PRs if independent.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: exposing guard() schemas to plugins via the new getFlattenedRoutes() method, matching the core objective.
Linked Issues check ✅ Passed The PR implements the core objective from issue #1490: guard-defined schemas are now exposed to plugins through getFlattenedRoutes(), enabling OpenAPI and similar tools to access these schemas.
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

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.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 11, 2025

Open in StackBlitz

npm i https://pkg.pr.new/elysiajs/elysia@1542

commit: 78e2115

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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95277d7 and 651470e.

📒 Files selected for processing (2)
  • src/index.ts (2 hunks)
  • test/core/flattened-routes.test.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
test/core/flattened-routes.test.ts (1)
src/index.ts (1)
  • t (8275-8275)
src/index.ts (2)
src/types.ts (2)
  • InternalRoute (1878-1886)
  • AnyLocalHook (1680-1680)
src/schema.ts (1)
  • mergeObjectSchemas (1403-1474)

@MarcelOlsen MarcelOlsen marked this pull request as draft November 11, 2025 14:07
@MarcelOlsen
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

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

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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 651470e and a4c480d.

📒 Files selected for processing (2)
  • src/index.ts (2 hunks)
  • test/core/flattened-routes.test.ts (1 hunks)

@MarcelOlsen
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

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

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: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4c480d and 648aef2.

📒 Files selected for processing (2)
  • src/index.ts (2 hunks)
  • test/core/flattened-routes.test.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/index.ts
🔇 Additional comments (2)
test/core/flattened-routes.test.ts (2)

1-5: LGTM! Clean test structure.

The imports are minimal and appropriate, and the test suite follows Bun's testing conventions.


39-49: Reference equality check assumes optimization.

The test correctly verifies that routes without guards are returned unchanged. Using .toBe() (reference equality) at line 48 assumes the implementation optimizes by returning the same object reference rather than cloning. This is a reasonable assumption, but note that if the implementation changes to always return new route objects, this assertion would need updating.

@MarcelOlsen
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Nov 11, 2025

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

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: 0

🧹 Nitpick comments (2)
test/core/flattened-routes.test.ts (2)

108-164: Consider verifying response[200] for completeness.

The standaloneValidator preservation test is thorough and addresses the past review feedback well. One minor enhancement: since the route provides a plain response schema (lines 124-127), and based on the merging behavior tested at lines 230-265, this should be merged as response[200].

Consider adding an assertion to verify this:

 		// Verify schemas were also flattened into direct properties
 		expect(protectedRoute?.hooks.headers).toBeDefined()
 		expect(protectedRoute?.hooks.headers.type).toBe('object')
 		expect(protectedRoute?.hooks.body).toBeDefined()
 		expect(protectedRoute?.hooks.body.type).toBe('object')
 		expect(protectedRoute?.hooks.response).toBeDefined()
+		expect(protectedRoute?.hooks.response[200]).toBeDefined()
+		expect(protectedRoute?.hooks.response[200].type).toBe('object')
+		expect(protectedRoute?.hooks.response[200].properties).toHaveProperty('success')
 		expect(protectedRoute?.hooks.response[401]).toBeDefined()
 		expect(protectedRoute?.hooks.response[500]).toBeDefined()

79-106: Optional: Consider testing schema type conflicts explicitly.

This test validates that guard headers and route body coexist (no conflict between different schema types). While the test at lines 230-265 shows route response precedence over guard response, you might want to add a test that explicitly covers the case where both guard and route define the same schema type (e.g., both define body).

This would clarify the expected precedence or merging behavior for non-response schema types:

it('route-level schema takes precedence over guard for same type', () => {
	const app = new Elysia().guard(
		{
			body: t.Object({
				guardField: t.String()
			})
		},
		(app) =>
			app.post('/override', ({ body }) => body, {
				body: t.Object({
					routeField: t.String()
				})
			})
	)

	// @ts-expect-error - accessing protected method for testing
	const flatRoutes = app.getFlattenedRoutes()
	const route = flatRoutes.find((r) => r.path === '/override')

	expect(route?.hooks.body).toBeDefined()
	// Verify which schema wins or how they're merged
	expect(route?.hooks.body.type).toBe('object')
	// Add assertions based on expected behavior
})
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 648aef2 and 78e2115.

📒 Files selected for processing (1)
  • test/core/flattened-routes.test.ts (1 hunks)
🔇 Additional comments (1)
test/core/flattened-routes.test.ts (1)

1-442: Excellent test coverage addressing past feedback!

The test suite is comprehensive and well-structured. It covers all the key scenarios including:

  • Basic guard schema merging across all schema types (body, headers, query, params, cookie, response)
  • Nested guards and guard+route schema combinations
  • Edge cases: unions, intersects, t.Any, and schemas without type properties
  • Status-code map merging with correct precedence rules
  • String schema aliases (TRef)

Most importantly, the standaloneValidator preservation test (lines 108-164) directly addresses the previous review feedback. Nice work!

@MarcelOlsen MarcelOlsen marked this pull request as ready for review November 11, 2025 14:53
@MarcelOlsen MarcelOlsen force-pushed the fix/expose-guard-schemas-clean branch from 569074e to 78e2115 Compare November 11, 2025 16:40
@SaltyAom
Copy link
Member

Nice work!

But if I understand this correctly, this would only work with Elysia.t?
If so, I would really recommend fixing the #1490 in OpenAPI level rather than application level since we can merge OpenAPI schema there

@SaltyAom SaltyAom self-assigned this Nov 13, 2025
@MarcelOlsen
Copy link
Contributor Author

Thanks for the feedback!

I see your point about keeping this at the OpenAPI level. My thinking was that other plugins (Swagger, Scalar, GraphQL schema generators) might also need access to guard schemas, so putting the merging logic in core would let them all reuse it instead of duplicating.

But you know the ecosystem better than I do - if OpenAPI is the only plugin that needs this, or if each plugin should handle its own merging, I'm happy to move the logic there. Would keep core simpler.

Also wondering about the "only work with Elysia.t" question - the implementation uses TypeBox schema merging, but the flattening itself works with any validator. Should this support other schema validators, or is TypeBox-only fine?

Let me know how you'd prefer to handle this and I'll refactor accordingly.

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.

OpenAPI: schemas defined via guard() are missing from generated spec

2 participants