better-drizzle
Plugins

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 zod
pnpm add better-drizzle zod
yarn add better-drizzle zod
bun add better-drizzle zod

Usage

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:

KeyDefaultValidates
create, createManytruethe data payload
update, updateMany, updateEachtruedata and where
upserttruecreate, update, and where
upsertManytruedata and the conflict update
resulttrueresults of writes and reads, except count, exists, delete, and deleteMany
findMany, findFirst, findOne, findUniquefalsethe query args
count, exists, paginate, cursorfalsethe call args
delete, deleteManyfalsethe call args
queryfalseturns 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: false skips every check for that call, including result.
  • validate: true forces the payload or args check, and the result check 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
  • notNull without default becomes required on create
  • notNull with 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:

  • fields customizes or removes individual column schemas
  • create, update, select, where, query, orderBy, pagination, and upsert can omit, extend, or partial

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 password are valid during parsing but are not forwarded to the database write.
  • better-drizzle/zod is runtime validation. It complements, but does not replace, TypeScript types.

Exports

better-drizzle/zod exports zod (also the default export), version, and these types:

TypeDescribes
ZodPluginOptionsoptions passed to zod(...)
ZodPluginValidateOptionsthe validate map
ZodPluginBehaviorthe behavior block
ZodPluginTableSchemasConfigone entry of schemas
BetterDrizzleZodModelSchemasthe $zod object of a table
BetterDrizzleZodModelExtension{ $zod } added to each delegate
BetterDrizzleZodModelExtensionResolverthe per-table model extension resolver
ZodPluginQueryInputfindMany / findFirst / findOne / findUnique args without meta
ZodPluginPaginationInputpaginate args without meta
ZodPluginCursorInputcursor args without meta

On this page