Skip to main content
doc0blade47/drizzle-ormdocumentation home
Sign in

Drizzle Zod Integration

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Drizzle Zod Integration automatically generates modular Zod validation schemas directly from Drizzle ORM table and view definitions, bridging database schema declarations with runtime input validation. By analyzing column types, nullability constraints, default values, and generated identities, the integration produces precise Zod objects tailored for select, insert, and update operations. Custom schema factories allow developers to configure type coercion rules and custom Zod instances across operations. Sources: drizzle-zod/src/schema.ts:15-123, drizzle-zod/src/schema.types.ts:6-60

Schema Generation Architecture

Operation Condition Matrix

Public generator functions createSelectSchema, createInsertSchema, and createUpdateSchema analyze Drizzle ORM tables, views, and PostgreSQL enums to dynamically construct corresponding Zod validation schemas. The architecture relies on internal condition sets (selectConditions, insertConditions, and updateConditions) that dictate whether individual columns are omitted (never), marked optional (optional), or flagged as nullable (nullable) depending on the database operation.

Sources: drizzle-zod/src/schema.ts:76-120

Operation Rule SetOmitted Columns (never)Optional Columns (optional)Nullable Columns (nullable)
selectConditions() => false() => false!column.notNull
insertConditionsgenerated.type === 'always' || generatedIdentity.type === 'always'!column.notNull || (column.notNull && column.hasDefault)!column.notNull
updateConditionsgenerated.type === 'always' || generatedIdentity.type === 'always'() => true!column.notNull

Sources: drizzle-zod/src/schema.ts:76-92

Column and Entity Processing

The core schema builder resolves table and view columns via getColumns, which delegates to getTableColumns for table definitions and getViewSelectedFields for database views. Recursive execution handles nested relation structures and custom field groups.

function getColumns(tableLike: Table | View) {
	return isTable(tableLike) ? getTableColumns(tableLike) : getViewSelectedFields(tableLike);
}
 
function handleColumns(
	columns: Record<string, any>,
	refinements: Record<string, any>,
	conditions: Conditions,
	factory?: CreateSchemaFactoryOptions<
		Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
	>,
): z.ZodType {
	const columnSchemas: Record<string, z.ZodType> = {};
 
	for (const [key, selected] of Object.entries(columns)) {
		if (!is(selected, Column) && !is(selected, SQL) && !is(selected, SQL.Aliased) && typeof selected === 'object') {
			const columns = isTable(selected) || isView(selected) ? getColumns(selected) : selected;
			columnSchemas[key] = handleColumns(columns, refinements[key] ?? {}, conditions, factory);
			continue;
		}

Sources: drizzle-zod/src/schema.ts:15-34

Note

When processing PostgreSQL enums via createSelectSchema, the generator bypasses column inspection entirely, resolving directly to zod.enum(enum_.enumValues) or using a custom zodInstance supplied through createSchemaFactory. Sources: drizzle-zod/src/schema.ts:66-74, drizzle-zod/src/schema.ts:98-100

Schema Factory Configuration

The createSchemaFactory function accepts options containing a custom zodInstance or global coerce rules, returning scoped instances of createSelectSchema, createInsertSchema, and createUpdateSchema bound to those configurations.

export function createSchemaFactory<
	TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
>(options?: CreateSchemaFactoryOptions<TCoerce>) {
	const createSelectSchema: CreateSelectSchema<TCoerce> = (
		entity: Table | View | PgEnum<[string, ...string[]]>,
		refine?: Record<string, any>,
	) => {
		if (isPgEnum(entity)) {
			return handleEnum(entity, options);
		}
		const columns = getColumns(entity);
		return handleColumns(columns, refine ?? {}, selectConditions, options) as any;
	};
 
	const createInsertSchema: CreateInsertSchema<TCoerce> = (
		entity: Table,
		refine?: Record<string, any>,
	) => {
		const columns = getColumns(entity);
		return handleColumns(columns, refine ?? {}, insertConditions, options) as any;
	};
 
	const createUpdateSchema: CreateUpdateSchema<TCoerce> = (
		entity: Table,
		refine?: Record<string, any>,
	) => {
		const columns = getColumns(entity);
		return handleColumns(columns, refine ?? {}, updateConditions, options) as any;
	};
 
	return { createSelectSchema, createInsertSchema, createUpdateSchema };
}

Sources: drizzle-zod/src/schema.ts:121-152

Warning

Columns defined with generatedalways or generatedIdentity set to 'always' evaluate to true under conditions.never for both insert and update operations, causing them to be entirely stripped from the resulting Zod object schemas. Sources: drizzle-zod/src/schema.ts:83-83, drizzle-zod/src/schema.ts:89-89

Column Conversion and Dialect Mapping

Low-Level Column Mapping

The columnToSchema function inspects individual Drizzle column definitions to produce corresponding Zod type instances. For columns with defined enumerations, it constructs an enum schema using z.enum(column.enumValues), falling back to a standard string schema if the list of values is empty.

export function columnToSchema(
	column: Column,
	factory:
		| CreateSchemaFactoryOptions<
			Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
		>
		| undefined,
): zod.ZodType {
	const z: typeof zod = factory?.zodInstance ?? zod;
	const coerce = factory?.coerce ?? {};
	let schema!: zod.ZodType;
 
	if (isWithEnum(column)) {
		schema = column.enumValues.length ? z.enum(column.enumValues) : z.string();
	}
 

Sources: drizzle-zod/src/column.ts:70-85

PostgreSQL geometric, vector, and line types receive specialized structural mapping. Tuple-based representations map to fixed-length Zod tuples, whereas object-based representations map to explicit Zod records with named coordinate keys.

if (!schema) {
	// Handle specific types
	if (isColumnType<PgGeometry<any> | PgPointTuple<any>>(column, ['PgGeometry', 'PgPointTuple'])) {
		schema = z.tuple([z.number(), z.number()]);
	} else if (
		isColumnType<PgPointObject<any> | PgGeometryObject<any>>(column, ['PgGeometryObject', 'PgPointObject'])
	) {
		schema = z.object({ x: z.number(), y: z.number() });
	} else if (isColumnType<PgHalfVector<any> | PgVector<any>>(column, ['PgHalfVector', 'PgVector'])) {
		schema = z.array(z.number());
		schema = column.dimensions ? (schema as zod.ZodArray<any>).length(column.dimensions) : schema;
	} else if (isColumnType<PgLineTuple<any>>(column, ['PgLine'])) {
		schema = z.tuple([z.number(), z.number(), z.number()]);
	} else if (isColumnType<PgLineABC<any>>(column, ['PgLineABC'])) {
		schema = z.object({
			a: z.number(),
			b: z.number(),
			c: z.number(),
		});
	} // Handle other types

Sources: drizzle-zod/src/column.ts:86-105

Note

Vector and array columns inspect column.dimensions or column.size properties to apply exact length validation constraints via Zod array length modifiers. Sources: drizzle-zod/src/column.ts:94-96, drizzle-zod/src/column.ts:106-108

DataType and Coercion Handling

For standard data types, column mapping resolves primitive categories (number, bigint, boolean, date, string, json, custom, and buffer) and applies optional coercion settings defined via createSchemaFactory.

	} else if (column.dataType === 'array') {
		schema = z.array(z.any());
	} else if (column.dataType === 'number') {
		schema = numberColumnToSchema(column, z, coerce);
	} else if (column.dataType === 'bigint') {
		schema = bigintColumnToSchema(column, z, coerce);
	} else if (column.dataType === 'boolean') {
		schema = coerce === true || coerce.boolean ? z.coerce.boolean() : z.boolean();
	} else if (column.dataType === 'date') {
		schema = coerce === true || coerce.date ? z.coerce.date() : z.date();
	} else if (column.dataType === 'string') {
		schema = stringColumnToSchema(column, z, coerce);
	} else if (column.dataType === 'json') {
		schema = jsonSchema;
	} else if (column.dataType === 'custom') {
		schema = z.any();
	} else if (column.dataType === 'buffer') {
		schema = bufferSchema;
	}
}

Sources: drizzle-zod/src/column.ts:109-128

Column DataTypeDefault Zod MappingCoercion Rule (coerce.* or true)
numbernumberColumnToSchemaHandled internally by numeric range/integer logic
bigintbigintColumnToSchemaHandled internally by bigint parsing rules
booleanz.boolean()z.coerce.boolean()
datez.date()z.coerce.date()
stringstringColumnToSchemaHandled internally by string configuration
jsonjsonSchemaUnaffected by scalar coercion
customz.any()Unaffected by scalar coercion
bufferbufferSchemaUnaffected by scalar coercion

Sources: drizzle-zod/src/column.ts:111-127

Warning

If a column datatype matches none of the recognized branches and has no associated enum structure, columnToSchema defaults the resulting validation target to z.any(). Sources: drizzle-zod/src/column.ts:130-132

Enum Normalization and SQL Translation

PostgreSQL enums processed outside table definitions via handleEnum extract enum values directly from enum_.enumValues using the active Zod instance.

function handleEnum(
	enum_: PgEnum<any>,
	factory?: CreateSchemaFactoryOptions<
		Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
	>,
) {
	const zod: typeof z = factory?.zodInstance ?? z;
	return zod.enum(enum_.enumValues);
}

Sources: drizzle-zod/src/schema.ts:66-74

Numeric columns inspect their SQL type definitions through column.getSQLType() to detect unsigned flags, establishing strict min and max boundaries corresponding to database integer widths.

function numberColumnToSchema(
	column: Column,
	z: typeof zod,
	coerce: CreateSchemaFactoryOptions<
		Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
	>['coerce'],
): zod.ZodType {
	let unsigned = column.getSQLType().includes('unsigned');
	let min!: number;
	let max!: number;
	let integer = false;
 
	if (isColumnType<MySqlTinyInt<any> | SingleStoreTinyInt<any>>(column, ['MySqlTinyInt', 'SingleStoreTinyInt'])) {
		min = unsigned ? 0 : CONSTANTS.INT8_MIN;
		max = unsigned ? CONSTANTS.INT8_UNSIGNED_MAX : CONSTANTS.INT8_MAX;
		integer = true;

Sources: drizzle-zod/src/column.ts:137-152

Type Inference and Internal Constraints

Compile-Time Schema Types and Utility Helpers

The compilation phase orchestrates how database tables, columns, and relations are mapped into strongly-typed Zod object schemas via recursive helper types. The core builder utility BuildSchema accepts an operation mode ('select' | 'insert' | 'update'), a record of columns, refinement configurations, and coercion rules. It iterates through table keys, applying generated-column filters where generated-always-as columns are restricted to select schemas while remaining present or omitted across other operation types.

export type BuildSchema<
	TType extends 'select' | 'insert' | 'update',
	TColumns extends Record<string, any>,
	TRefinements extends Record<string, any> | undefined,
	TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
> = z.ZodObject<
	Simplify<
		{
			[
				K in keyof TColumns as ColumnIsGeneratedAlwaysAs<TColumns[K]> extends true ? TType extends 'select' ? K
					: never
					: K
			]: TColumns[K] extends infer TColumn extends Column
				? IsRefinementDefined<TRefinements, K> extends true
					? Assume<HandleRefinement<TType, TRefinements[K & keyof TRefinements], TColumn>, z.ZodType>
				: HandleColumn<TType, TColumn, TCoerce>
				: TColumns[K] extends infer TObject extends SelectedFieldsFlat<Column> | Table | View ? BuildSchema<
						TType,
						GetSelection<TObject>,
						TRefinements extends object ? TRefinements[K & keyof TRefinements] : undefined,
						TCoerce
					>
				: z.ZodAny;
		}
	>,
	{ out: {}; in: {} }
>;

Sources: drizzle-zod/src/schema.types.internal.ts:40-66

The framework evaluates column nullability and default states through conditional wrapper types like HandleSelectColumn, HandleInsertColumn, and HandleUpdateColumn. Required select fields retain their underlying Zod schema types, whereas nullable columns wrap them in z.ZodNullable. Insert and update helpers inject z.ZodOptional based on whether columns possess default values or allow null assignment.

type HandleSelectColumn<
	TSchema extends z.ZodType,
	TColumn extends Column,
> = TColumn['_']['notNull'] extends true ? TSchema
	: z.ZodNullable<TSchema>;
 
type HandleInsertColumn<
	TSchema extends z.ZodType,
	TColumn extends Column,
> = TColumn['_']['notNull'] extends true ? TColumn['_']['hasDefault'] extends true ? z.ZodOptional<TSchema>
	: TSchema
	: z.ZodOptional<z.ZodNullable<TSchema>>;
 
type HandleUpdateColumn<
	TSchema extends z.ZodType,
	TColumn extends Column,
> = TColumn['_']['notNull'] extends true ? z.ZodOptional<TSchema>
	: z.ZodOptional<z.ZodNullable<TSchema>>;
 

Sources: drizzle-zod/src/column.types.ts:76-94

Operation ModeNullable & No DefaultNot-Null & Has DefaultNot-Null & No Default
selectz.ZodNullable<TSchema>TSchemaTSchema
insertz.ZodOptional<z.ZodNullable<TSchema>>z.ZodOptional<TSchema>TSchema
updatez.ZodOptional<z.ZodNullable<TSchema>>z.ZodOptional<TSchema>z.ZodOptional<TSchema>

Sources: drizzle-zod/src/column.types.ts:76-94

Note

Selection fields supporting nested relations or embedded objects recursively invoke BuildSchema against sub-selections derived from GetSelection<TObject>. Sources: drizzle-zod/src/schema.types.internal.ts:56-61

Guardrails and DrizzleTypeError Protection

To prevent invalid schema configurations at compile time, internal type assertions use DrizzleTypeError guardrails. When schema refinements introduce keys that do not exist on the target comparison type, NoUnknownKeys intercepts the invalid property and triggers a descriptive compile-time failure.

export type NoUnknownKeys<
	TRefinement extends Record<string, any>,
	TCompare extends Record<string, any>,
> = {
	[K in keyof TRefinement]: K extends keyof TCompare
		? TRefinement[K] extends Record<string, z.ZodType> ? NoUnknownKeys<TRefinement[K], TCompare[K]>
		: TRefinement[K]
		: DrizzleTypeError<`Found unknown key in refinement: "${K & string}"`>;
};

Sources: drizzle-zod/src/schema.types.internal.ts:68-76

Type coercion validation relies on the CanCoerce helper, which checks whether global coercion is enabled (true) or if specific primitive flags within the partial coercion record are marked as true for types like bigint, boolean, date, number, or string.

type CanCoerce<
	TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
	TTo extends 'bigint' | 'boolean' | 'date' | 'number' | 'string',
> = TCoerce extends true ? true
	: TCoerce extends Record<string, any> ? TCoerce[TTo] extends true ? true
		: false
	: false;

Sources: drizzle-zod/src/column.types.ts:36-42

Warning

Passing unregistered keys inside refinement objects causes NoUnknownKeys to evaluate the property against DrizzleTypeError, halting compilation with an explicit error message naming the offending key. Sources: drizzle-zod/src/schema.types.internal.ts:68-76

Custom Overrides and Refinement Pipelines

Column Refinements and Direct Replacements

The schema generation pipeline accepts an optional refinement record as its second parameter, allowing developers to customize individual column validation rules or completely replace generated Zod types. During schema building in handleColumns, the generator checks whether a refinement is defined for a given column key. If the refinement value is provided and is not a function, it directly overrides the column schema. If the refinement is defined as a function, it is invoked with the automatically generated column schema as its argument, enabling chaining or custom validation transformations.

function handleColumns(
	columns: Record<string, any>,
	refinements: Record<string, any>,
	conditions: Conditions,
	factory?: CreateSchemaFactoryOptions<
		Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
	>,
): z.ZodType {
	const columnSchemas: Record<string, z.ZodType> = {};
 
	for (const [key, selected] of Object.entries(columns)) {
		if (!is(selected, Column) && !is(selected, SQL) && !is(selected, SQL.Aliased) && typeof selected === 'object') {
			const columns = isTable(selected) || isView(selected) ? getColumns(selected) : selected;
			columnSchemas[key] = handleColumns(columns, refinements[key] ?? {}, conditions, factory);
			continue;
		}
 
		const refinement = refinements[key];
		if (refinement !== undefined && typeof refinement !== 'function') {
			columnSchemas[key] = refinement;
			continue;
		}
 
		const column = is(selected, Column) ? selected : undefined;
		const schema = column ? columnToSchema(column, factory) : z.any();
		const refined = typeof refinement === 'function' ? refinement(schema) : schema;
 

Sources: drizzle-zod/src/schema.ts:19-45

const userInsertSchema = createInsertSchema(users, {
  email: (schema) => schema.email(),
  age: z.number().min(18),
});

Sources: drizzle-zod/src/schema.ts:105-111

Note

Refinement functions receive the exact base schema generated by columnToSchema, preserving nullability and optional wrapping unless explicitly overridden inside the transform callback. Sources: drizzle-zod/src/schema.ts:42-45

Nested Structures and View Field Selections

When processing tables, views, or relational selections, handleColumns inspects entry values recursively. If an entry key represents a nested object or view rather than a primitive column or SQL expression, the generator retrieves the underlying column map via getColumns—which evaluates getTableColumns for tables and getViewSelectedFields for database views—and processes the nested record recursively using the matching refinement subsection.

function getColumns(tableLike: Table | View) {
	return isTable(tableLike) ? getTableColumns(tableLike) : getViewSelectedFields(tableLike);
}
 
function handleColumns(
	columns: Record<string, any>,
	refinements: Record<string, any>,
	conditions: Conditions,
	factory?: CreateSchemaFactoryOptions<
		Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
	>,
): z.ZodType {
	const columnSchemas: Record<string, z.ZodType> = {};
 
	for (const [key, selected] of Object.entries(columns)) {
		if (!is(selected, Column) && !is(selected, SQL) && !is(selected, SQL.Aliased) && typeof selected === 'object') {
			const columns = isTable(selected) || isView(selected) ? getColumns(selected) : selected;
			columnSchemas[key] = handleColumns(columns, refinements[key] ?? {}, conditions, factory);
			continue;
		}

Sources: drizzle-zod/src/schema.ts:15-34

Type checking for these nested refinement structures is enforced at compile time through recursive conditional typing. The BuildRefine type utility maps over columns and nested relations, validating that any provided refinement matches either a valid Zod type or a transformation function accepting the corresponding column type schema.

export type BuildRefine<
	TColumns extends Record<string, any>,
	TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
> = {
	[K in keyof TColumns as TColumns[K] extends Column | SelectedFieldsFlat<Column> | Table | View ? K : never]?:
		TColumns[K] extends Column ? BuildRefineField<GetZodType<TColumns[K], TCoerce>>
			: BuildRefine<GetSelection<TColumns[K]>, TCoerce>;
};

Sources: drizzle-zod/src/schema.types.internal.ts:14-21

Warning

Supplying an unknown key inside a nested refinement tree trips the NoUnknownKeys helper, halting type compilation with a DrizzleTypeError indicating the illegal property name. Sources: drizzle-zod/src/schema.types.internal.ts:68-76

Form Handling and Runtime Validation

Input Mutation Parsing and Mutation Validation

Generated Zod schemas integrate into application form handling and API request validation pipelines by defining runtime parsers for database mutations. Using createInsertSchema and createUpdateSchema, applications validate incoming payloads before executing database operations. The condition rules built into insertConditions and updateConditions automatically omit database-generated columns—such as columns configured with always generation or identity defaults—while enforcing required fields, nullability, and optional defaults for client-supplied payloads.

const insertConditions: Conditions = {
	never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always',
	optional: (column) => !column.notNull || (column.notNull && column.hasDefault),
	nullable: (column) => !column.notNull,
};
 
const updateConditions: Conditions = {
	never: (column) => column?.generated?.type === 'always' || column?.generatedIdentity?.type === 'always',
	optional: () => true,
	nullable: (column) => !column.notNull,
};
 

Sources: drizzle-zod/src/schema.ts:82-93

const insertUserSchema = createInsertSchema(users);
const validatedData = insertUserSchema.parse(requestBody);

Sources: drizzle-zod/src/schema.ts:105-111

Caution

Columns defined with generatedAlwaysAs or always identity are completely excluded from insert and update schemas via conditions.never, causing Zod to strip these fields if submitted in client mutation payloads. Sources: drizzle-zod/src/schema.ts:83-89

Query Result and View Validation

For read operations and user interface rendering, createSelectSchema generates runtime validation contracts that reflect exact table structures and database views. Unlike insertion handlers, select schemas evaluate columns against selectConditions, preserving all table fields while mapping nullable database columns to Zod nullable wrappers without marking them optional. This ensures API responses and UI form initializers match the exact shape queried from the database.

const selectConditions: Conditions = {
	never: () => false,
	optional: () => false,
	nullable: (column) => !column.notNull,
};

Sources: drizzle-zod/src/schema.ts:76-80

const selectUserSchema = createSelectSchema(users);
const userProfile = selectUserSchema.parse(dbRecord);

Sources: drizzle-zod/src/schema.ts:94-103

Note

When passed a database view or an enumerated type directly, createSelectSchema resolves view selected fields via getViewSelectedFields or builds a Zod enum parser from enumValues. Sources: drizzle-zod/src/schema.ts:15-17, drizzle-zod/src/schema.ts:98-100

Was this page helpful?