Zod
Generate per-table Zod schemas from your Drizzle schema and validate Better Drizzle operations at runtime.
better-drizzle/zod generates Zod schemas directly from your Better Drizzle schema and uses them to validate payloads, query args, and results through plugin hooks.
Install
npm install better-drizzle zodpnpm add better-drizzle zodyarn add better-drizzle zodbun add better-drizzle zodUsage
import { better } from 'better-drizzle';
import { z } from 'zod';
import { zod } from 'better-drizzle/zod';
const client = better(db, {
schema,
plugins: [
zod({
validate: {
create: true,
update: true,
upsert: true,
result: true,
},
behavior: {
coerce: true,
unknownKeys: 'strip',
},
schemas: {
users: {
fields: {
email: (schema) => schema.email().toLowerCase(),
name: (schema) => schema.min(2),
passwordHash: false,
},
create: {
omit: ['id', 'passwordHash'],
extend: {
password: z.string().min(8),
},
},
update: {
omit: ['id', 'passwordHash'],
partial: true,
},
select: {
omit: ['passwordHash'],
},
},
},
}),
],
});$zod on every delegate
Each table delegate exposes generated schemas:
client.users.$zod.create;
client.users.$zod.update;
client.users.$zod.upsert;
client.users.$zod.select;
client.users.$zod.where;
client.users.$zod.orderBy;
client.users.$zod.pagination;
client.users.$zod.query;Use them directly when you want shared validation at the edges of your app:
const input = client.users.$zod.create.parse(req.body);Validation behavior
Validation runs in plugin hooks. validate in the plugin options turns each check on or off:
| Key | Default | Validates |
|---|---|---|
create, createMany | true | the data payload |
update, updateMany, updateEach | true | data and where |
upsert | true | create, update, and where |
upsertMany | true | data and the conflict update |
result | true | results of writes and reads, except count, exists, delete, and deleteMany |
findMany, findFirst, findOne, findUnique | false | the query args |
count, exists, paginate, cursor | false | the call args |
delete, deleteMany | false | the call args |
query | false | turns on args validation for every read, delete, and deleteMany |
Parsed write payloads replace the originals, so coercion and schema transforms reach the database.
Per call
Every delegate operation accepts a typed validate?: boolean arg: findMany, findFirst, findOne, findUnique, count, exists, paginate, cursor, create, createMany, update, updateMany, updateEach, upsert, upsertMany, delete, and deleteMany.
validate: falseskips every check for that call, includingresult.validate: trueforces the payload or args check, and theresultcheck where the operation has one.- omitted, the plugin options (or the defaults above) apply.
await client.users.create({
data: req.body,
validate: false,
});Validation errors
A failed parse throws a BetterDrizzleError with code OPERATION_ERROR, table set to the schema key, and a message such as Zod validation failed for create payload on "users". The Zod issues are in details:
{
pluginId: 'better-drizzle/zod',
issues: [
{ code: 'invalid_type', message: 'Invalid input: expected string, received number', path: 'name' },
],
}path is the Zod issue path joined with ., for example data.email. The exact message text comes from your Zod version.
import { BetterDrizzleError } from 'better-drizzle';
const isZodError = (error: unknown): error is BetterDrizzleError =>
error instanceof BetterDrizzleError &&
error.details?.pluginId === 'better-drizzle/zod';Schema customization
The plugin starts from database columns and then applies operation rules:
- generated columns are omitted from write schemas
notNullwithout default becomes required on createnotNullwith default becomes optional on create- nullable columns become
nullable().optional()for create - update starts from the create schema and becomes partial
Then your per-table overrides are applied:
fieldscustomizes or removes individual column schemascreate,update,select,where,query,orderBy,pagination, andupsertcanomit,extend, orpartial
Coercion and unknown keys
behavior.coerce enables input coercion for supported scalar types, and behavior.unknownKeys controls whether Zod strips, rejects, or passes through unknown keys.
zod({
behavior: {
coerce: true,
unknownKeys: 'strip',
},
});Notes
- The plugin strips non-column keys before handing write payloads to Drizzle.
- Schema-only extension fields such as
passwordare valid during parsing but are not forwarded to the database write. better-drizzle/zodis runtime validation. It complements, but does not replace, TypeScript types.
Exports
better-drizzle/zod exports zod (also the default export), version, and these types:
| Type | Describes |
|---|---|
ZodPluginOptions | options passed to zod(...) |
ZodPluginValidateOptions | the validate map |
ZodPluginBehavior | the behavior block |
ZodPluginTableSchemasConfig | one entry of schemas |
BetterDrizzleZodModelSchemas | the $zod object of a table |
BetterDrizzleZodModelExtension | { $zod } added to each delegate |
BetterDrizzleZodModelExtensionResolver | the per-table model extension resolver |
ZodPluginQueryInput | findMany / findFirst / findOne / findUnique args without meta |
ZodPluginPaginationInput | paginate args without meta |
ZodPluginCursorInput | cursor args without meta |