Skip to content

feat(router): support Valibot schema validation#43

Merged
halvaradop merged 2 commits intomasterfrom
feat/add-valibot-support
May 7, 2026
Merged

feat(router): support Valibot schema validation#43
halvaradop merged 2 commits intomasterfrom
feat/add-valibot-support

Conversation

@halvaradop
Copy link
Copy Markdown
Member

@halvaradop halvaradop commented May 7, 2026

Description

Adds support for Valibot schema validation alongside the existing Zod integration for route validation.

Validation schemas can now be defined using Valibot for:

  • route parameters
  • search parameters
  • request body content

This change was introduced as part of the work in aura-stack-ts/auth#160 and expands compatibility with additional schema validation libraries such as Valibot, ArkType, and future adapters.

Key Changes

  • Add Valibot schema validation support
  • Enable Valibot-based inference for endpoint schemas
  • Extend validation support beyond Zod

Usage

import * as valibot from "valibot"

import {
  createEndpoint,
  createRouter,
  createClient,
} from "@aura-stack/router"

const credentials = createEndpoint(
  "POST",
  "/auth/credentials",
  (ctx) => {
    return ctx.json({ success: true })
  },
  {
    schemas: {
      body: valibot.object({
        username: valibot.string(),
        password: valibot.string(),
      }),
    },
  }
)

const router = createRouter([credentials])

const client = createClient<typeof router>({
  baseURL: "http://api.example.com",
})

await client.post("/auth/credentials", {
  body: {
    username: "johndoe",
    password: "1234567890",
  },
})

@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
router Ready Ready Preview, Comment May 7, 2026 10:16pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

Warning

Rate limit exceeded

@halvaradop has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 15 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 96cfc587-33d5-48de-96b2-19ca3c32ee97

📥 Commits

Reviewing files that changed from the base of the PR and between eba65e0 and 2403409.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • package.json
  • src/assert.ts
  • src/context.ts
  • src/types.ts
  • src/validator/registry.ts
  • test/client.test.ts
  • test/type.test-d.ts
📝 Walkthrough

Walkthrough

This PR extends the router framework to support Valibot as an alternative schema validation library alongside Zod. A unified validator abstraction layer (SchemaAdapter and createValidator) normalizes validation across both libraries, while type inference is updated throughout to infer types from either Zod or Valibot schemas.

Changes

Add Valibot Schema Support

Layer / File(s) Summary
Dependency & Type Guards
package.json, src/assert.ts
Adds valibot as a devDependency and introduces isZodSchema and isValibotSchema runtime type guards to detect schema objects by their internal structure (_def for Zod, ~run for Valibot).
Validation Abstraction
src/validator/registry.ts
Defines ValidationResult<T> discriminated union and SchemaAdapter<T> interface with a unified validate(data) contract. createValidator<T>(schema) factory detects and wraps both Zod and Valibot schemas, mapping their respective safeParse results into a uniform result shape.
Type Inference for Schemas
src/types.ts
Widens EndpointSchemas to accept ZodObject | Valibot ObjectSchema for body, searchParams, and params. Extends ContextSearchParams, ContextBody, and ContextParams to infer types from either library using z.infer or Valibot InferOutput, with fallbacks to URLSearchParams, undefined, and route defaults.
Runtime Validation Integration
src/context.ts
Replaces direct safeParse calls in getRouteParams, getSearchParams, and getBody with the unified createValidator(...).validate(...) abstraction while preserving existing error formatting and exception handling.
Test Coverage for Valibot
test/endpoint.test.ts, test/type.test-d.ts
Adds runtime tests for Valibot body, search params, and route params validation (POST/GET endpoints with valid/invalid input, 422 assertions). Extends type-check tests with Valibot ObjectSchema inference scenarios for body, search params, and params including enum and string field types.

🐰 A validator for two
Zod and Valibot dance as one,
Unified, now schemas run!
Type-safe paths beneath the sun,
Validation's work is never done. 🌟

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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: adding Valibot schema validation support to the router, which is reflected across all modified files including new type guards, validator registry, and comprehensive test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-valibot-support

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
Copy Markdown

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/types.ts (1)

291-292: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

HasSchemas ignores Valibot schemas, breaking Client type inference for Valibot-based endpoints.

type HasSchemas<C> =
    C extends EndpointConfig<any, infer Schemas> ? (Schemas[keyof Schemas] extends ZodObject<any> ? true : false) : false

When body, searchParams, or params is a Valibot ObjectSchema, HasSchemas returns false, so the Client type falls back to the untyped (path, ctx?) => ... signature and loses the schema-derived request shape. The fix is to also recognize Valibot schemas:

-type HasSchemas<C> =
-    C extends EndpointConfig<any, infer Schemas> ? (Schemas[keyof Schemas] extends ZodObject<any> ? true : false) : false
+type HasSchemas<C> =
+    C extends EndpointConfig<any, infer Schemas>
+        ? Schemas[keyof Schemas] extends ZodObject<any> | ObjectSchema<any, undefined>
+            ? true
+            : false
+        : false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types.ts` around lines 291 - 292, The HasSchemas conditional only checks
for ZodObject instances and therefore returns false for Valibot schemas,
breaking Client inference; update HasSchemas (the conditional on EndpointConfig
and Schemas) to also accept Valibot's ObjectSchema type (e.g., check
Schemas[keyof Schemas] extends ZodObject<any> | ObjectSchema<any>) so
Valibot-based body/searchParams/params produce true and restore the typed Client
signature; ensure you import the Valibot ObjectSchema type and use that union in
the conditional where HasSchemas is defined.
src/context.ts (1)

45-55: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

formatZodError is called with Valibot Issue[], producing empty details: {} for all Valibot validation failures.

When Valibot validation fails, registry.ts sets error: result.issues — a valibot.Issue[]. formatZodError receives that array, checks error.issues (which is undefined on an array), immediately returns {}, and every Valibot error response has empty details. This is a silent loss of validation detail for Valibot users.

The same problem exists in getSearchParams (Line 97) and getBody (Line 128).

A minimal fix is a dedicated Valibot formatter:

+import type { BaseIssue } from "valibot"
+
+export const formatValibotError = (issues: BaseIssue<unknown>[]) => {
+    if (!issues || issues.length === 0) return {}
+    return issues.reduce((prev, issue) => {
+        const key = issue.path?.map((p) => p.key).join(".") ?? ""
+        return { ...prev, [key]: { kind: issue.kind, message: issue.message } }
+    }, {})
+}

Then in each call site, branch on the schema type:

-            throw new InvalidZodSchemaError("UNPROCESSABLE_ENTITY", formatZodError(parsed.error))
+            throw new InvalidZodSchemaError(
+                "UNPROCESSABLE_ENTITY",
+                Array.isArray(parsed.error) ? formatValibotError(parsed.error) : formatZodError(parsed.error)
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context.ts` around lines 45 - 55, getRouteParams/getSearchParams/getBody
call formatZodError with a Valibot Issue[] (set in registry.ts) which makes
formatZodError return empty details; add a Valibot-specific formatter (e.g.,
formatValibotError) that converts valibot.Issue[] into the same details shape
expected by InvalidZodSchemaError, then change the error handling in
getRouteParams, getSearchParams and getBody to detect the validator/schema type
produced by createValidator and branch: when the validation result is a Valibot
result call formatValibotError(result.issues) and when it's a Zod result call
formatZodError(result.error), and throw InvalidZodSchemaError using the properly
formatted details so Valibot failures are surfaced correctly.
package.json (1)

85-95: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

valibot (and zod) should be optional peerDependencies, not devDependencies.

Both are used in published source files (src/validator/registry.ts, src/types.ts, src/assert.ts). With tsup, packages in devDependencies are bundled into the output, forcing a fixed version on all consumers and preventing them from tree-shaking or supplying their own version. Since support for each library is optional and detected at runtime, both should be declared as optional peer dependencies:

+  "peerDependencies": {
+    "zod": "^4.1.11",
+    "valibot": "^1.4.0"
+  },
+  "peerDependenciesMeta": {
+    "zod": { "optional": true },
+    "valibot": { "optional": true }
+  },
   "devDependencies": {
     ...
-    "zod": "^4.1.11",
-    "valibot": "^1.4.0"
+    "zod": "^4.1.11",
+    "valibot": "^1.4.0"
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 85 - 95, Move zod and valibot out of
devDependencies and declare them as optional peerDependencies so consumers can
provide their own versions and tree-shake properly: remove "zod" and "valibot"
from devDependencies in package.json, add them under "peerDependencies" with
appropriate semver ranges, and mark them optional via "peerDependenciesMeta"
(set each to { "optional": true }). Ensure any runtime detection code in
src/validator/registry.ts, src/types.ts, and src/assert.ts continues to handle
absence of these libs gracefully (no hard imports), and update packaging/config
(tsup) if necessary so these libs are not bundled into the distributed build.
🧹 Nitpick comments (1)
src/assert.ts (1)

3-4: 💤 Low value

BaseSchema should use a type-only import.

BaseSchema is a TypeScript interface in valibot with no runtime value; importing it as a value forces a side-effectful module evaluation at startup.

-import { BaseSchema } from "valibot"
-import { ZodObject } from "zod"
+import type { BaseSchema } from "valibot"
+import type { ZodObject } from "zod"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/assert.ts` around lines 3 - 4, The import of BaseSchema should be
type-only to avoid forcing runtime evaluation; update the import in
src/assert.ts to use a type import for BaseSchema (e.g., "import type {
BaseSchema } from 'valibot'") while leaving ZodObject as a regular import so any
runtime value remains available; ensure all usages of BaseSchema in
functions/types continue to compile as a type-only import.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/assert.ts`:
- Around line 90-92: isZodSchema currently only checks for "_def" which breaks
for Zod v4; update the predicate in isZodSchema to detect both Zod v3 and v4
internals (e.g., return true if value is a non-null object and contains "_zod"
or "_def", or specifically checks for value["_zod"]?.def) so ZodObject schemas
are recognized across versions and validation in registry.ts works again.

In `@src/validator/registry.ts`:
- Around line 29-32: The thrown "Unsupported schema type" should not be
swallowed and converted into a validation failure: update the code in
registry.ts so the unsupported-schema check either occurs before the try block
or rethrows the error instead of returning { success:false,... } inside the
catch; specifically, ensure the branch that detects an unsupported schema type
(the throw new Error("Unsupported schema type")) is moved out of the try/catch
or that the catch rethrows when e.message === "Unsupported schema type" (or when
the error is not a validation parse result), so callers in context.ts receive an
actual thrown error rather than a 422 result; keep the existing switch to using
result.success for the Valibot SafeParseResult handling.

In `@test/type.test-d.ts`:
- Around line 78-87: Remove or use the dead type alias `Nose`: either delete the
`type Nose = RequestContext<...>` declaration if it's not needed, or replace its
usage where appropriate (e.g., annotate a test helper or parameter with `Nose`)
so the `Nose` alias is referenced; locate the `Nose` declaration and update code
that uses `RequestContext`/`ZodObject`/`ZodString` types to consume this alias
if intended to be kept.

---

Outside diff comments:
In `@package.json`:
- Around line 85-95: Move zod and valibot out of devDependencies and declare
them as optional peerDependencies so consumers can provide their own versions
and tree-shake properly: remove "zod" and "valibot" from devDependencies in
package.json, add them under "peerDependencies" with appropriate semver ranges,
and mark them optional via "peerDependenciesMeta" (set each to { "optional":
true }). Ensure any runtime detection code in src/validator/registry.ts,
src/types.ts, and src/assert.ts continues to handle absence of these libs
gracefully (no hard imports), and update packaging/config (tsup) if necessary so
these libs are not bundled into the distributed build.

In `@src/context.ts`:
- Around line 45-55: getRouteParams/getSearchParams/getBody call formatZodError
with a Valibot Issue[] (set in registry.ts) which makes formatZodError return
empty details; add a Valibot-specific formatter (e.g., formatValibotError) that
converts valibot.Issue[] into the same details shape expected by
InvalidZodSchemaError, then change the error handling in getRouteParams,
getSearchParams and getBody to detect the validator/schema type produced by
createValidator and branch: when the validation result is a Valibot result call
formatValibotError(result.issues) and when it's a Zod result call
formatZodError(result.error), and throw InvalidZodSchemaError using the properly
formatted details so Valibot failures are surfaced correctly.

In `@src/types.ts`:
- Around line 291-292: The HasSchemas conditional only checks for ZodObject
instances and therefore returns false for Valibot schemas, breaking Client
inference; update HasSchemas (the conditional on EndpointConfig and Schemas) to
also accept Valibot's ObjectSchema type (e.g., check Schemas[keyof Schemas]
extends ZodObject<any> | ObjectSchema<any>) so Valibot-based
body/searchParams/params produce true and restore the typed Client signature;
ensure you import the Valibot ObjectSchema type and use that union in the
conditional where HasSchemas is defined.

---

Nitpick comments:
In `@src/assert.ts`:
- Around line 3-4: The import of BaseSchema should be type-only to avoid forcing
runtime evaluation; update the import in src/assert.ts to use a type import for
BaseSchema (e.g., "import type { BaseSchema } from 'valibot'") while leaving
ZodObject as a regular import so any runtime value remains available; ensure all
usages of BaseSchema in functions/types continue to compile as a type-only
import.
🪄 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: 34cd61c6-b17d-47d5-be1e-0fd5e005bc77

📥 Commits

Reviewing files that changed from the base of the PR and between 30a93e7 and eba65e0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • package.json
  • src/assert.ts
  • src/context.ts
  • src/types.ts
  • src/validator/registry.ts
  • test/endpoint.test.ts
  • test/type.test-d.ts

Comment thread src/assert.ts
Comment thread src/validator/registry.ts Outdated
Comment thread test/type.test-d.ts Outdated
Comment thread test/client.test.ts
@@ -1,5 +1,6 @@
import { describe, test, expect, vi, beforeEach, expectTypeOf } from "vitest"
import { z } from "zod"
import { never, z } from "zod"
@halvaradop halvaradop merged commit 5407fa9 into master May 7, 2026
6 checks passed
@halvaradop halvaradop deleted the feat/add-valibot-support branch May 7, 2026 22:32
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