# Zod

> Generate per-table Zod schemas from your Drizzle schema and validate Better Drizzle operations at runtime.

URL: https://better-drizzle.com/docs/plugins/zod

`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**

```bash
npm install better-drizzle zod
```

**pnpm**

```bash
pnpm add better-drizzle zod
```

**yarn**

```bash
yarn add better-drizzle zod
```

**bun**

```bash
bun add better-drizzle zod
```

## Usage

```ts
import { better } from 'better-drizzle';
import { z } from 'zod';
import { zod } from 'better-drizzle/zod';

const client = better(db, {
	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:

```ts
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:

```ts
const input = client.users.$zod.create.parse(req.body);
```

The schema types come from the client, so `zod()` needs no type argument; options passed inline are reflected in the types (an omitted column disappears from `z.output<typeof client.users.$zod.create>`). Pass `zod<typeof relations>(options)` when you want the `schemas` table keys and column names checked while writing the options.

Fields added with `extend` (such as `password` above) may be passed in `data` alongside columns; the plugin validates them and strips them before Drizzle writes.

## 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: 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.

```ts
await client.users.create({
	data: req.body,
	validate: false,
});
```

### Validation errors

A failed parse throws a `BetterDrizzleError` with code `INVALID_ARGS` (status 400), `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`:

```ts
// error.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.

```ts
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.

```ts
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:

| 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                                                 |
| `ZodModelExtensionResolver`              | the per-table model extension resolver (a `ModelExtensionTypeResolver` interface) |
| `BetterDrizzleZodModelExtensionResolver` | legacy generic-function form of the resolver                                      |
| `ZodPluginQueryInput`                    | `findMany` / `findFirst` / `findOne` / `findUnique` args without `meta`           |
| `ZodPluginPaginationInput`               | `paginate` args without `meta`                                                    |
| `ZodPluginCursorInput`                   | `cursor` args without `meta`                                                      |
