Type-safe Convex functions with Zod v4 schemas. Codecs in the schema — zx.date(), custom transformations — plus typed IDs, all wired up through initZodvex.
Built on top of convex-helpers
- Why zodvex?
- Installation
- Quick Start
- Import Paths
- Features
- Supported Types
- Upgrading?
- API Reference
- Roadmap
- License
- Codec-aware database —
zx.date(),zx.codec()encode/decode automatically atctx.dbboundaries - Correct optional/nullable semantics — preserves Convex's distinction (
.optional()→v.optional(T),.nullable()→v.union(T, v.null())) - Client-safe models —
defineZodModelis importable in React - End-to-end type safety — same schema from database to frontend forms
| Feature | zodvex | convex-helpers/zod4 |
|---|---|---|
| Codec-aware DB | initZodvex wraps ctx.db automatically |
Manual |
| Date handling | zx.date() — automatic Date ↔ timestamp |
Manual z.codec() |
| Typed IDs | zx.id('table') with type branding |
Manual |
| Custom codecs | zx.codec() with auto-detection |
Not provided |
| Client-safe models | defineZodModel (importable in React) |
Not provided |
| Codegen | Optional typed hooks, boundary helpers | Not provided |
Both are valid choices — zodvex trades some explicitness for significantly better ergonomics.
npm install zodvex zod convex convex-helpersPeer dependencies:
zod(^4.3.6)convex(^1.28.0)convex-helpers(^0.1.104)- TypeScript 5.x recommended
// convex/models.ts
import { z } from 'zod'
import { zx, defineZodModel } from 'zodvex'
export const EventModel = defineZodModel('events', {
title: z.string(),
startDate: zx.date(),
endDate: zx.date().optional(),
location: z.string().optional(),
})
defineZodModelis client-safe — you can import it in React.
// convex/schema.ts
import { defineZodSchema } from 'zodvex/server'
import { EventModel } from './models'
export default defineZodSchema({
events: EventModel,
})// convex/functions.ts
import { initZodvex } from 'zodvex/server'
import {
query, mutation, action,
internalQuery, internalMutation, internalAction,
} from './_generated/server'
import schema from './schema'
export const { zq, zm, za, ziq, zim, zia } = initZodvex(schema, {
query, mutation, action,
internalQuery, internalMutation, internalAction,
})initZodvex returns builders for all six Convex function types. By default, zq and zm (and internal variants) wrap ctx.db with automatic codec encode/decode.
// convex/events.ts
import { z } from 'zod'
import { zx } from 'zodvex'
import { zq, zm } from './functions'
import { EventModel } from './models'
export const list = zq({
args: {},
returns: EventModel.schema.docArray,
handler: async (ctx) => {
// Dates come back as Date objects, not numbers
return await ctx.db.query('events').collect()
},
})
export const create = zm({
args: {
title: z.string(),
startDate: zx.date(),
endDate: zx.date().optional(),
location: z.string().optional(),
},
returns: zx.id('events'),
handler: async (ctx, args) => {
// Dates are automatically encoded to timestamps on write
return await ctx.db.insert('events', args)
},
})
zx.id('events')is a typed Convex ID validator — it provides type branding forGenericId<'events'>but is NOT a codec (no wire transformation happens).zx.date()andzx.codec()ARE codecs.
See the full quickstart example for a runnable project.
Four entry points:
zodvex— Client-safe full-Zod surface. Use in React components and shared code.zodvex/server— Server-only. Use in Convex functions and schema definitions.zodvex/mini— Client-safe zod/mini surface.zodvex/mini/server— Server-only zod/mini surface.zodvex/legacy— Deprecated runtime APIs kept only for migration.
zodvex/coreremains as a deprecated compatibility alias forzodvex.
initZodvex wraps ctx.db so reads decode automatically and writes encode automatically.
zx.date()— Date ↔ timestamp codec. Stored asv.float64(), used asDatein handlers.zx.codec(wire, runtime, transforms)— Custom codecs for complex transformations (encryption, serialization, etc.).zx.id('table')— Typed Convex ID validator withGenericId<T>branding. This is NOT a codec — no wire transformation happens.
Guides: Custom Codecs, Date Handling
zodvex includes an optional CLI that generates typed client code:
- Typed hooks —
useZodQuery,useZodMutationwith automatic codec decode - Boundary helpers —
encodeArgs,decodeResultfor custom client integrations - Action auto-decode —
ctx.runQuery/ctx.runMutationdecode via registry
zodvex generate # one-shot generation
zodvex dev # watch modeWhen you need codegen: Full-stack apps with React frontends wanting typed client hooks.
When you DON'T: Server-side codec-aware DB works with just initZodvex — no codegen needed.
Guides: Codegen | Example: examples/task-manager/
zodTable and zQueryBuilder still work without initZodvex:
import { zodTable, zQueryBuilder } from 'zodvex/legacy'
import { query } from './_generated/server'
const Users = zodTable('users', { name: z.string() })
const zq = zQueryBuilder(query)At this level, zodvex is roughly equivalent to convex-helpers. This is a valid stepping-stone, but the API is deprecated. When you're ready for codecs, see the Quick Start above.
| Zod Type | Convex Validator |
|---|---|
z.string() |
v.string() |
z.number() |
v.float64() |
z.bigint() |
v.int64() |
z.boolean() |
v.boolean() |
z.null() |
v.null() |
z.array(T) |
v.array(T) |
z.object({...}) |
v.object({...}) |
z.record(T) |
v.record(v.string(), T) |
z.union([...]) |
v.union(...) |
z.literal(x) |
v.literal(x) |
z.enum(['a', 'b']) |
v.union(v.literal('a'), v.literal('b')) ¹ |
z.optional(T) |
v.optional(T) |
z.nullable(T) |
v.union(T, v.null()) |
Note: Native
z.date()is not supported — usezx.date()instead. See Date Handling for details.
Zod v4 Enum Type Note:
¹ Enum types in Zod v4 produce a slightly different TypeScript signature than manually created unions:
// Manual union (precise tuple type)
const manual = v.union(v.literal('a'), v.literal('b'))
// Type: VUnion<"a" | "b", [VLiteral<"a", "required">, VLiteral<"b", "required">], "required", never>
// From Zod enum (array type)
const fromZod = zodToConvex(z.enum(['a', 'b']))
// Type: VUnion<"a" | "b", Array<VLiteral<"a" | "b", "required">>, "required", never>This difference is purely cosmetic with no functional impact:
- Value types are identical (
"a" | "b") - Runtime validation is identical
- Type safety for function arguments/returns is preserved
- Convex uses
T[number]which works identically for both array and tuple types
This limitation exists because Zod v4 changed enum types from tuple-based to Record-based (ToEnum<T>). TypeScript cannot convert a Record type to a specific tuple without knowing the keys at compile time. See Zod v4 changelog and enum evolution discussion for more details.
zx namespace types:
import { zx } from 'zodvex'
// Convex IDs — typed validator, NOT a codec
zx.id('tableName') // → v.id('tableName')
zx.id('tableName').optional() // → v.optional(v.id('tableName'))
// Dates — codec (Date ↔ timestamp)
zx.date() // → v.float64() (timestamp)
zx.date().optional() // → v.optional(v.float64())
zx.date().nullable() // → v.union(v.float64(), v.null())
// Custom codecs
zx.codec(wireSchema, runtimeSchema, { encode, decode })Upgrading from a previous version? Read the migration guide for what changed and why. Key takeaway: the CLI/codegen is optional — the Quick Start path above needs no codegen.
Automated renames are available:
npx zodvex migrate ./convex # apply renames
npx zodvex migrate ./convex --dry-run # preview changes- The zx Namespace —
zx.id(),zx.date(),zx.codec() - Builders —
initZodvexand legacy builders - Custom Context —
.withContext(),onSuccess - Custom Codecs —
zx.codec(),decodeDoc/encodeDoc - Date Handling —
zx.date()deep dive - Form Validation — react-hook-form integration
- Working with Subsets —
.pick(),.fields - Mapping Helpers —
zodToConvex,zodToConvexFields - Return Type Helpers —
returnsAs - Large Schemas —
pickShape,safePick - Polymorphic Tables — Union/discriminated union tables
- AI SDK Compatibility — Vercel AI SDK integration
- Codegen — CLI, registry, typed hooks
- Migration tooling: vanilla Convex → zodvex (for new adopters with existing Convex projects)
- Migration tooling: pre-0.5 → current zodvex
- Additional example projects (e.g., full-stack with React, codegen showcase)
- Per-feature READMEs in examples/task-manager/
MIT
Built with ❤️ on top of convex-helpers