@coji/zodix

中文文档
0.7.0发布于 2025/11/01MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 2292

Parse React Router loaders and actions with Zod

原始使用文档
文档目录27 节
  1. Highlights
  2. Setup
  3. Zod Version Compatibility
  4. Migration Guide
  5. Usage
  6. zx.parseParams(params: Params, schema: Schema)
  7. zx.parseForm(request: Request, schema: Schema)
  8. zx.parseQuery(request: Request, schema: Schema)
  9. zx.parseParamsSafe() / zx.parseFormSafe() / zx.parseQuerySa…
  10. Error Handling
  11. parseParams() , parseForm() , and parseQuery()
  12. parseParamsSafe() , parseFormSafe() , and parseQuerySafe()
  13. Dynamic Schemas
  14. Type Inference Helper
  15. Building Dynamic Schemas with z.object().extend()
  16. Alternative Approaches
  17. Helper Zod Schemas
  18. Available Helpers
  19. Usage
  20. Zod v3/v4 Compatibility Details
  21. How It Works
  22. Using with Zod v3
  23. Using with Zod v4
  24. Extras
  25. Custom URLSearchParams parsing
  26. Actions with Multiple Intents
  27. Acknowledgments

Zodix

Build Status npm version

Zodix is a collection of Zod utilities for React Router v7 loaders and actions. It abstracts the complexity of parsing and validating FormData and URLSearchParams so your loaders/actions stay clean and are strongly typed.

✨ Now with full support for both Zod v3 and v4!

React Router loaders often look like:

export async function loader({ params, request }: Route.LoaderArgs) {
  const { id } = params
  const url = new URL(request.url)
  const count = url.searchParams.get('count') || '10'
  if (typeof id !== 'string') {
    throw new Error('id must be a string')
  }
  const countNumber = parseInt(count, 10)
  if (isNaN(countNumber)) {
    throw new Error('count must be a number')
  }
  // Fetch data with id and countNumber
}

Here is the same loader with Zodix:

export async function loader({ params, request }: Route.LoaderArgs) {
  const { id } = zx.parseParams(params, { id: z.string() })
  const { count } = zx.parseQuery(request, { count: zx.NumAsString })
  // Fetch data with id and count
}

Check the example app for complete examples of common patterns.

Highlights

  • Full Zod v3 and v4 compatibility - Works seamlessly with both versions
  • Dynamic schema support - Build schemas at runtime based on database/API data
  • Significantly reduce React Router action/loader bloat
  • Avoid the oddities of FormData and URLSearchParams
  • Tiny with no external dependencies (Less than 1kb gzipped)
  • Use existing Zod schemas, or write them on the fly
  • Custom Zod schemas for stringified numbers, booleans, and checkboxes
  • Throw errors meant for React Router error boundaries by default
  • Supports non-throwing parsing for custom validation/errors
  • Works with all React Router runtimes (Node, Deno, Vercel, Cloudflare, etc)
  • Full unit test coverage

Setup

Install with npm, yarn, pnpm, etc.

npm install @coji/zodix zod

Zod Version Compatibility

Zodix supports both Zod v3 and v4 through separate import paths:

  • Zod v3: Use @coji/zodix (requires zod@^3.25.0 or later)
  • Zod v4: Use @coji/zodix/v4 (requires zod@^4.0.0)
// For Zod v3
import { zx } from '@coji/zodix'

// For Zod v4
import { zx } from '@coji/zodix/v4'

Migration Guide

Upgrading from Zod v3 to v4? Follow these steps:

  1. Update your Zod dependency: npm install zod@^4.0.0
  2. Review and migrate your Zod schemas if needed - see Zod v4 Changelog for breaking changes
  3. Change your Zodix imports from @coji/zodix to @coji/zodix/v4
  4. That's it! 🎉

Usage

Import the zx object, or specific functions:

import { zx } from '@coji/zodix'
// import { parseParams, NumAsString } from '@coji/zodix';

zx.parseParams(params: Params, schema: Schema)

Parse and validate the Params object from Route.LoaderArgs['params'] or Route.ActionArgs['params'] using a Zod shape:

export async function loader({ params }: Route.LoaderArgs) {
  const { userId, noteId } = zx.parseParams(params, {
    userId: z.string(),
    noteId: z.string(),
  })
}

The same as above, but using an existing Zod object schema:

// This is if you have many pages that share the same params.
export const ParamsSchema = z.object({ userId: z.string(), noteId: z.string() })

export async function loader({ params }: Route.LoaderArgs) {
  const { userId, noteId } = zx.parseParams(params, ParamsSchema)
}

zx.parseForm(request: Request, schema: Schema)

Parse and validate FormData from a Request in a React Router action and avoid the tedious FormData dance:

export async function action({ request }: Route.ActionArgs) {
  const { email, password, saveSession } = await zx.parseForm(request, {
    email: z.string().email(),
    password: z.string().min(6),
    saveSession: zx.CheckboxAsString,
  })
}

Integrate with existing Zod schemas and models/controllers:

// db.ts
export const CreateNoteSchema = z.object({
  userId: z.string(),
  title: z.string(),
  category: NoteCategorySchema.optional(),
})

export function createNote(note: z.infer<typeof CreateNoteSchema>) {}
import { CreateNoteSchema, createNote } from './db'

export async function action({ request }: Route.ActionArgs) {
  const formData = await zx.parseForm(request, CreateNoteSchema)
  createNote(formData) // No TypeScript errors here
}

zx.parseQuery(request: Request, schema: Schema)

Parse and validate the query string (search params) of a Request:

export async function loader({ request }: Route.LoaderArgs) {
  const { count, page } = zx.parseQuery(request, {
    // NumAsString parses a string number ("5") and returns a number (5)
    count: zx.NumAsString,
    page: zx.NumAsString,
  })
}

zx.parseParamsSafe() / zx.parseFormSafe() / zx.parseQuerySafe()

These work the same as the non-safe versions, but don't throw when validation fails. They use z.parseSafe() and always return an object with the parsed data or an error.

export async function action(args: Route.ActionArgs) {
  const results = await zx.parseFormSafe(args.request, {
    email: z.string().email({ message: 'Invalid email' }),
    password: z
      .string()
      .min(8, { message: 'Password must be at least 8 characters' }),
  })
  return {
    success: results.success,
    error: results.error,
  }
}

Check the login page example for a full example.

Error Handling

parseParams(), parseForm(), and parseQuery()

These functions throw a 400 Response when the parsing fails. This works nicely with React Router error boundaries and should be used for parsing things that should rarely fail and don't require custom error handling. You can pass a custom error message or status code.

export async function loader({ params }: Route.LoaderArgs) {
  const { postId } = zx.parseParams(
    params,
    { postId: zx.NumAsString },
    { message: "Invalid postId parameter", status: 400 }
  );
  const post = await getPost(postId);
  return { post };
}
export function ErrorBoundary() {
  const error = useRouteError();
  return <h1>Error: {error.statusText}</h1>;
}

Check the post page example for a full example.

parseParamsSafe(), parseFormSafe(), and parseQuerySafe()

These functions are great for form validation because they don't throw when parsing fails. They always return an object with this shape:

{ success: boolean; error?: ZodError; data?: <parsed data>; }

You can then handle errors in the action and access them in the component using useActionData(). Check the login page example for a full example.

Dynamic Schemas

Sometimes you need to build schemas dynamically based on runtime data (e.g., filter options from a database). Zodix provides excellent support for this pattern.

Type Inference Helper

Use the InferParams type helper to get explicit types from your schemas:

import { zx, type InferParams } from '@coji/zodix'

const mySchema = z.object({
  q: z.string().optional(),
  page: zx.IntAsString.optional(),
})

type Params = InferParams<typeof mySchema>
// → { q: string | undefined, page: number | undefined }

Building Dynamic Schemas with z.object().extend()

The recommended approach for dynamic schemas is using z.object().extend():

export async function loader({ request }: Route.LoaderArgs) {
  // Fetch dynamic filter categories from database/API
  const filterCategories = await getAvailableFilterCategories()
  // → [{ id: 'category', ... }, { id: 'brand', ... }, ...]

  // 1. Define base schema with known fields
  const baseSchema = z.object({
    q: z.string().optional(),
    page: zx.IntAsString.optional(),
  })

  // 2. Build dynamic fields from runtime data
  const dynamicFields = Object.fromEntries(
    filterCategories.map((cat) => [cat.id, z.string().optional()]),
  )

  // 3. Extend base schema with dynamic fields
  const fullSchema = baseSchema.extend(dynamicFields)

  // 4. Parse with full type inference (Zod v4)
  const parsed = zx.parseQuery(request, fullSchema)
  // ✨ parsed.q is string | undefined
  // ✨ parsed.page is number | undefined
  // ✨ parsed[category.id] is string | undefined
}

Note: With Zod v3, type inference for dynamic fields may return any due to TypeScript limitations. For better type safety with v3, consider alternative approaches.

Alternative Approaches

For more advanced use cases and alternative patterns (URLSearchParams, JSON Record, etc.), see the Dynamic Schemas Guide. This guide covers:

  • Three different approaches with trade-offs
  • Zod v3 vs v4 considerations
  • Real-world examples (e-commerce filters, admin dashboards, multi-tenant SaaS)
  • Best practices for centralized schema definitions

Check the dynamic filters example for a complete working implementation.

Helper Zod Schemas

Because FormData and URLSearchParams serialize all values to strings, you often end up with things like "5", "on" and "true". The helper schemas handle parsing and validating strings representing other data types and are meant to be used with the parse functions.

Available Helpers

zx.BoolAsString

  • "true" → true
  • "false" → false
  • "notboolean" → throws ZodError

zx.CheckboxAsString

  • "on" → true
  • undefined → false
  • "anythingbuton" → throws ZodError

zx.IntAsString

  • "3" → 3
  • "3.14" → throws ZodError
  • "notanumber" → throws ZodError

zx.NumAsString

  • "3" → 3
  • "3.14" → 3.14
  • "notanumber" → throws ZodError

See the tests for more details.

Usage

const Schema = z.object({
  isAdmin: zx.BoolAsString,
  agreedToTerms: zx.CheckboxAsString,
  age: zx.IntAsString,
  cost: zx.NumAsString,
})

const parsed = Schema.parse({
  isAdmin: 'true',
  agreedToTerms: 'on',
  age: '38',
  cost: '10.99',
})

/*
parsed = {
  isAdmin: true,
  agreedToTerms: true,
  age: 38,
  cost: 10.99
}
*/

Zod v3/v4 Compatibility Details

How It Works

Zodix provides separate import paths for Zod v3 and v4 compatibility:

  1. Use the appropriate import path based on your Zod version
  2. Full type safety is maintained for both versions
  3. Same API across both versions - only the import path changes

Using with Zod v3

import { z } from 'zod' // v3.x
import { zx } from '@coji/zodix' // Default path for v3

const schema = z.object({
  name: z.string(),
  age: zx.IntAsString,
})

export async function loader({ params }: Route.LoaderArgs) {
  const data = zx.parseParams(params, schema) // Works with Zod v3!
}

Using with Zod v4

import { z } from 'zod' // v4.x
import { zx } from '@coji/zodix/v4' // Use v4 path

const schema = z.object({
  name: z.string(),
  age: zx.IntAsString,
})

export async function loader({ params }: Route.LoaderArgs) {
  const data = zx.parseParams(params, schema) // Works with Zod v4!
}

Extras

Custom URLSearchParams parsing

You may have URLs with query string that look like ?ids[]=1&ids[]=2 or ?ids=1,2 that aren't handled as desired by the built in URLSearchParams parsing.

You can pass a custom function, or use a library like query-string to parse them with Zodix.

// Create a custom parser function
type ParserFunction = (
  params: URLSearchParams,
) => Record<string, string | string[]>
const customParser: ParserFunction = () => {
  /* ... */
}

// Parse non-standard search params
const search = new URLSearchParams(`?ids[]=id1&ids[]=id2`)
const { ids } = zx.parseQuery(
  request,
  { ids: z.array(z.string()) },
  { parser: customParser },
)

// ids = ['id1', 'id2']

Actions with Multiple Intents

Zod discriminated unions are great for helping with actions that handle multiple intents like this:

// This adds type narrowing by the intent property
const Schema = z.discriminatedUnion('intent', [
  z.object({ intent: z.literal('delete'), id: z.string() }),
  z.object({ intent: z.literal('create'), name: z.string() }),
])

export async function action({ request }: Route.ActionArgs) {
  const data = await zx.parseForm(request, Schema)
  switch (data.intent) {
    case 'delete':
      // data is now narrowed to { intent: 'delete', id: string }
      return { success: true }
    case 'create':
      // data is now narrowed to { intent: 'create', name: string }
      return { success: true }
    default:
      // data is now narrowed to never. This will error if a case is missing.
      const _exhaustiveCheck: never = data
  }
}

Acknowledgments

This project is a fork of rileytomasek/zodix. Thanks to Riley Tomasek for creating the original Zodix library.

版本与发布记录

@coji/zodix 共发布 3 个版本,首次发布于 2025/08/07;过去 12 个月发布了 0 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.7.0latest2025/11/011 年前
0.6.22025/11/011 年前
0.5.02025/08/071.3 年前

关于 @coji/zodix 的常见问题

@coji/zodix 是什么?

Parse React Router loaders and actions with Zod

如何安装 @coji/zodix?

在项目目录运行 npm install @coji/zodix,或使用 pnpm add / yarn add / bun add。

@coji/zodix 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@coji/zodix 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@coji/zodix 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@coji/zodix 可以商用吗?

@coji/zodix 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@coji/zodix 的下载量有多少?

根据 npm 官方下载统计,@coji/zodix 在 2026/10/11 – 2026/11/09 这 30 天内被下载 2,292 次,日均约 76 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@coji/zodix 还在维护吗?

最新版本 0.7.0 发布于 2025/11/01(1 年前);至今共发布 3 个版本,其中过去 12 个月发布了 0 个;首次发布于 2025/08/07。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。