Skip to content

fix(server-hono): reuse active Zod instance for Swagger schemas#1224

Merged
omeraplak merged 2 commits intomainfrom
fix/swagger-openapi-fallback
Apr 22, 2026
Merged

fix(server-hono): reuse active Zod instance for Swagger schemas#1224
omeraplak merged 2 commits intomainfrom
fix/swagger-openapi-fallback

Conversation

@omeraplak
Copy link
Copy Markdown
Member

@omeraplak omeraplak commented Apr 22, 2026

PR Checklist

Please check if your PR fulfills the following requirements:

Bugs / Features

What is the current behavior?

server-hono composes some OpenAPI route schemas with its own zod-openapi-compat Zod instance, while nested schemas are imported as already-created Zod schemas from server-core.

When package resolution gives those packages different Zod instances or major versions, the Zod v4 OpenAPI generator can reject nested schemas with errors like Invalid element at key "data": expected a Zod schema. That causes /doc generation to fail and Swagger UI to show an empty API list.

What is the new behavior?

server-core schema definitions can now be created through a schema factory. server-hono uses that factory with the active Zod instance from zod-openapi-compat, so built-in Swagger route schemas are composed from one compatible Zod instance.

This fixes the OpenAPI generation failure directly instead of adding a Swagger fallback path.

fixes #1222

Notes for reviewers

Validation run:

  • pnpm --filter @voltagent/server-core build
  • pnpm --filter @voltagent/server-hono build
  • pnpm --filter @voltagent/server-hono test -- src/app-factory.spec.ts src/utils/custom-endpoints.spec.ts
  • pnpm --filter @voltagent/server-core test -- src/schemas/agent.schemas.spec.ts
  • pnpm exec biome check .changeset/smart-swagger-paths.md packages/server-core/src/schemas/agent.schemas.ts packages/server-hono/src/routes/agent.routes.ts packages/server-hono/src/app-factory.spec.ts

I also reproduced the issue from examples/base and verified /doc stays populated with Zod v3, Zod v4, and the previously failing mixed Zod resolution case.

Docs were not updated because this is a runtime bug fix for existing Swagger behavior.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved API documentation endpoint stability and reliability by implementing consistent schema handling across the application to prevent documentation generation failures that could occur from conflicting schema instances or version mismatches.
  • Tests

    • Added OpenAPI compatibility tests to validate that the documentation endpoint functions properly and schemas are managed consistently throughout the application platform.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 22, 2026

🦋 Changeset detected

Latest commit: be34b04

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@voltagent/server-hono Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@joggrbot

This comment has been minimized.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

A factory function createServerCoreSchemas() was introduced to parameterize schema definitions by a chosen Zod instance, enabling the OpenAPI generation layer to reuse a single Zod version. This addresses schema-instance mixing that caused empty Swagger endpoints. Routes and tests updated to call the factory instead of importing static schemas.

Changes

Cohort / File(s) Summary
Changeset & Versioning
.changeset/smart-swagger-paths.md
Added changeset documenting patch release for @voltagent/server-hono to fix Swagger/OpenAPI generation by enforcing single Zod instance reuse.
Schema Factory Refactoring
packages/server-core/src/schemas/agent.schemas.ts
Converted module from top-level schema constants to createServerCoreSchemas(zod?) factory function. Schema definitions moved inside factory, parameterized by provided Zod namespace. Default exports recreated via factory call at module scope to preserve backward compatibility.
Route Integration
packages/server-hono/src/routes/agent.routes.ts
Updated to import and call createServerCoreSchemas(z) instead of importing individual schemas. Re-export schemas from factory-returned objects instead of directly from @voltagent/server-core.
OpenAPI Compatibility Testing
packages/server-hono/src/app-factory.spec.ts
Added test verifying OpenAPI 3.1 document generation succeeds when schemas are created via createServerCoreSchemas(zodV4), ensuring /agents route OpenAPI output is properly constructed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • ci(changesets): version packages #1221: Addresses Zod v4 compatibility in @voltagent/server-hono's OpenAPI generator, fixing record-schema handling that complements this single Zod-instance approach.

Poem

🐰 Schemas once lived as static friends,
But Zod versions made their logic bend,
Now factories spawn with chosen z,
One instance rules—Swagger API!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the primary change: reusing the active Zod instance for Swagger schemas to fix schema compatibility issues.
Description check ✅ Passed The PR description completes all required sections with concrete details about current behavior, new behavior, issue linkage, tests, changesets, and validation steps.
Linked Issues check ✅ Passed The code changes fully address issue #1222 by implementing a factory function to reuse the active Zod instance, ensuring OpenAPI schemas use compatible Zod versions and preventing generation failures.
Out of Scope Changes check ✅ Passed All changes are directly scoped to resolving the linked issue: factory function implementation, schema imports consolidation, and test coverage with no extraneous modifications.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/swagger-openapi-fallback

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.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 22, 2026

Deploying voltagent with  Cloudflare Pages  Cloudflare Pages

Latest commit: be34b04
Status: ✅  Deploy successful!
Preview URL: https://85b258a9.voltagent.pages.dev
Branch Preview URL: https://fix-swagger-openapi-fallback.voltagent.pages.dev

View logs

Copy link
Copy Markdown
Contributor

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

🧹 Nitpick comments (3)
packages/server-hono/src/utils/custom-endpoints.ts (2)

312-379: Fallback reconstruction logic looks solid.

The branch ordering (built-ins first, then extractCustomEndpoints), the seenRoutes dedup, and the seenRoutes.size > 0 ? fallbackDoc : baseDoc guard behave sensibly, and the try/catch keeps /doc responsive even on a bad app.routes shape. Two small observations:

  • Line 336: toOpenApiPath(routePath) === "/ui"/ui has no :param, so the conversion is a no-op; routePath === "/ui" would read clearer and could be folded into the guard on line 332.
  • Line 285 (in applyRouteDefinitionMetadata, referenced here for context): assigning operation.description = route.description when route.description is undefined will wipe the descriptive placeholder set by addEndpointToDoc. Consider if (route.description) operation.description = route.description; for parity with the operationId handling.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-hono/src/utils/custom-endpoints.ts` around lines 312 - 379,
The toOpenApiPath("/ui") check in getFallbackOpenApiDocFromRoutes is redundant
and should be replaced by checking the raw routePath (fold into the existing
guard that skips "*", "/", "/doc") — update the conditional that currently reads
toOpenApiPath(routePath) === "/ui" to instead check routePath === "/ui" and
merge it with the earlier guard; also update applyRouteDefinitionMetadata so it
does not overwrite placeholder descriptions from addEndpointToDoc by changing
the unconditional assignment operation.description = route.description to only
assign when route.description is defined (e.g. if (route.description)
operation.description = route.description), keeping the existing operationId
handling behavior for parity.

201-256: Consider normalizing the path key to OpenAPI {param} form inside addEndpointToDoc.

The helper uses the raw path as the doc.paths key (line 212) but derives parameters from the converted pathWithBraces (line 241). When this helper is reached via getEnhancedOpenApiDoccustomEndpoints.forEach(... addEndpointToDoc(fullDoc, endpoint)) for a custom Hono route like /users/:id, the resulting OpenAPI document gets paths["/users/:id"] with a parameter named id — a non-compliant template that Swagger UI may render with the literal colon. The fallback path avoids this only because it passes openApiPath explicitly. Normalizing once in the helper keeps both call sites correct:

♻️ Proposed refactor
-function addEndpointToDoc(doc: any, endpoint: ServerEndpointSummary, pathOverride?: string) {
-  const path = pathOverride ?? endpoint.path;
-  const method = endpoint.method.toLowerCase();
+function addEndpointToDoc(doc: any, endpoint: ServerEndpointSummary, pathOverride?: string) {
+  const rawPath = pathOverride ?? endpoint.path;
+  const path = toOpenApiPath(rawPath);
+  const method = endpoint.method.toLowerCase();
@@
-  const pathWithBraces = toOpenApiPath(path);
-  if (pathWithBraces.includes("{")) {
-    const params = pathWithBraces.match(/\{([^}]+)\}/g);
+  if (path.includes("{")) {
+    const params = path.match(/\{([^}]+)\}/g);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-hono/src/utils/custom-endpoints.ts` around lines 201 - 256,
In addEndpointToDoc: normalize the paths key to OpenAPI brace form by computing
const openApiPath = toOpenApiPath(path) (or use pathOverride if it was already
intended), and use openApiPath when reading/creating doc.paths (replace
doc.paths[path] and pathObj derived from it) and when building the
summary/description/parameters; keep extracting parameters from openApiPath
(previously pathWithBraces) and preserve existing tags/description logic but
reference openApiPath for the doc.paths key and any path-display text so the
stored path uses {param} syntax consistently.
packages/server-hono/src/app-factory.spec.ts (1)

287-300: Test correctly exercises the fallback path.

Overriding app.getOpenAPIDocument to throw is a clean way to simulate the schema-generation failure in #1222, and asserting both /agents and the :name{name} conversion covers the two behaviors called out in the PR description. Optional: add a custom route via configureApp and assert it also appears in the fallback doc — that would protect against regressions in the custom-endpoint branch of getFallbackOpenApiDocFromRoutes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-hono/src/app-factory.spec.ts` around lines 287 - 300, The
test should also verify the fallback behavior for custom routes: update the spec
that sets app.getOpenAPIDocument to throw (in the test using createApp and
createDeps) to pass a configureApp option that registers a custom route (via the
configureApp callback used by createApp), then assert the fallback OpenAPI doc
(returned for /doc) contains that custom path alongside "/agents" and the
TOOL_ROUTES entries; this ensures getFallbackOpenApiDocFromRoutes includes
custom endpoints and protects the custom-endpoint branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/server-hono/src/app-factory.spec.ts`:
- Around line 287-300: The test should also verify the fallback behavior for
custom routes: update the spec that sets app.getOpenAPIDocument to throw (in the
test using createApp and createDeps) to pass a configureApp option that
registers a custom route (via the configureApp callback used by createApp), then
assert the fallback OpenAPI doc (returned for /doc) contains that custom path
alongside "/agents" and the TOOL_ROUTES entries; this ensures
getFallbackOpenApiDocFromRoutes includes custom endpoints and protects the
custom-endpoint branch.

In `@packages/server-hono/src/utils/custom-endpoints.ts`:
- Around line 312-379: The toOpenApiPath("/ui") check in
getFallbackOpenApiDocFromRoutes is redundant and should be replaced by checking
the raw routePath (fold into the existing guard that skips "*", "/", "/doc") —
update the conditional that currently reads toOpenApiPath(routePath) === "/ui"
to instead check routePath === "/ui" and merge it with the earlier guard; also
update applyRouteDefinitionMetadata so it does not overwrite placeholder
descriptions from addEndpointToDoc by changing the unconditional assignment
operation.description = route.description to only assign when route.description
is defined (e.g. if (route.description) operation.description =
route.description), keeping the existing operationId handling behavior for
parity.
- Around line 201-256: In addEndpointToDoc: normalize the paths key to OpenAPI
brace form by computing const openApiPath = toOpenApiPath(path) (or use
pathOverride if it was already intended), and use openApiPath when
reading/creating doc.paths (replace doc.paths[path] and pathObj derived from it)
and when building the summary/description/parameters; keep extracting parameters
from openApiPath (previously pathWithBraces) and preserve existing
tags/description logic but reference openApiPath for the doc.paths key and any
path-display text so the stored path uses {param} syntax consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 28a71899-03df-405b-8f1a-4ff0c7ec1ac8

📥 Commits

Reviewing files that changed from the base of the PR and between a5dde84 and 0347eab.

📒 Files selected for processing (3)
  • .changeset/smart-swagger-paths.md
  • packages/server-hono/src/app-factory.spec.ts
  • packages/server-hono/src/utils/custom-endpoints.ts

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/server-hono/src/utils/custom-endpoints.ts">

<violation number="1" location="packages/server-hono/src/utils/custom-endpoints.ts:340">
P1: Fallback OpenAPI generation drops/mismatches built-in routes when the app uses `basePath`, because built-in route matching and emitted path keys do not preserve the runtime-prefixed path.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/server-hono/src/utils/custom-endpoints.ts Outdated
@omeraplak omeraplak changed the title fix(server-hono): keep Swagger docs populated on OpenAPI fallback fix(server-hono): reuse active Zod instance for Swagger schemas Apr 22, 2026
Copy link
Copy Markdown
Contributor

@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

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

Inline comments:
In `@packages/server-core/src/schemas/agent.schemas.ts`:
- Around line 1-5: The current ZodNamespace type (type ZodNamespace = typeof
defaultZ) pins createServerCoreSchemas to the default zod namespace and forces
callers using Zod v4 to cast to any; make the factory parameter generic so it
accepts other compatible Zod namespaces without unsafe casts. Replace the
ZodNamespace alias and update the function signature to a generic form such as
function createServerCoreSchemas<Z extends typeof defaultZ = typeof
defaultZ>(zod: Z = defaultZ as Z) (or provide an explicit overload/union of
supported namespace types), keeping references to defaultZ and
createServerCoreSchemas intact so callers using zodV4 can pass their namespace
without as any.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e15d623a-1e88-4dbc-b98a-aaad8be88276

📥 Commits

Reviewing files that changed from the base of the PR and between 0347eab and be34b04.

📒 Files selected for processing (4)
  • .changeset/smart-swagger-paths.md
  • packages/server-core/src/schemas/agent.schemas.ts
  • packages/server-hono/src/app-factory.spec.ts
  • packages/server-hono/src/routes/agent.routes.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • .changeset/smart-swagger-paths.md
  • packages/server-hono/src/app-factory.spec.ts

Comment on lines +1 to +5
import { z as defaultZ } from "zod";

type ZodNamespace = typeof defaultZ;

export function createServerCoreSchemas(zod: ZodNamespace = defaultZ) {
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the factory parameter is pinned to the default zod namespace.
rg -n -C3 'type\s+ZodNamespace\s*=\s*typeof\s+defaultZ|createServerCoreSchemas\(zod: ZodNamespace' packages/server-core/src/schemas/agent.schemas.ts

# Verify alternate Zod usage currently requires an unsafe cast.
rg -n -C3 'createServerCoreSchemas\(\s*zodV4\s+as\s+any\s*\)' packages/server-hono/src/app-factory.spec.ts

Repository: VoltAgent/voltagent

Length of output: 575


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for all usages of createServerCoreSchemas to see if this is a widespread issue
echo "=== All usages of createServerCoreSchemas ==="
rg -n 'createServerCoreSchemas\(' --type ts --type tsx

# Check if ZodNamespace is used elsewhere or redefined
echo -e "\n=== ZodNamespace references ==="
rg -n 'ZodNamespace' --type ts --type tsx

# Check if there's an existing union type or overloaded signatures
echo -e "\n=== Check for type definitions around the factory ==="
rg -n -A10 'export function createServerCoreSchemas' packages/server-core/src/schemas/agent.schemas.ts | head -20

Repository: VoltAgent/voltagent

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for all usages of createServerCoreSchemas
echo "=== All usages of createServerCoreSchemas ==="
rg -n 'createServerCoreSchemas\(' --type ts

# Check if ZodNamespace is used elsewhere or redefined
echo -e "\n=== ZodNamespace references ==="
rg -n 'ZodNamespace' --type ts

# Check the full function signature and surrounding context
echo -e "\n=== Function definition context ==="
rg -n -A5 'export function createServerCoreSchemas' packages/server-core/src/schemas/agent.schemas.ts

Repository: VoltAgent/voltagent

Length of output: 1012


Widen the factory parameter type so Zod v4 callers don't need as any.

ZodNamespace = typeof defaultZ pins createServerCoreSchemas to the default zod namespace type. The test demonstrates this requires an unsafe cast: createServerCoreSchemas(zodV4 as any). For a PR adding Zod v4 compatibility, this violates type safety at the call site. Define a proper union type or overload for the supported Zod namespaces instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/server-core/src/schemas/agent.schemas.ts` around lines 1 - 5, The
current ZodNamespace type (type ZodNamespace = typeof defaultZ) pins
createServerCoreSchemas to the default zod namespace and forces callers using
Zod v4 to cast to any; make the factory parameter generic so it accepts other
compatible Zod namespaces without unsafe casts. Replace the ZodNamespace alias
and update the function signature to a generic form such as function
createServerCoreSchemas<Z extends typeof defaultZ = typeof defaultZ>(zod: Z =
defaultZ as Z) (or provide an explicit overload/union of supported namespace
types), keeping references to defaultZ and createServerCoreSchemas intact so
callers using zodV4 can pass their namespace without as any.

@omeraplak omeraplak merged commit 8b09874 into main Apr 22, 2026
24 checks passed
@omeraplak omeraplak deleted the fix/swagger-openapi-fallback branch April 22, 2026 19:41
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.

[BUG] swagger is empty

1 participant