Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions drizzle-zod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"license": "Apache-2.0",
"peerDependencies": {
"drizzle-orm": ">=0.36.0",
"zod": ">=3.0.0"
"zod": "^3.25.0"
},
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.0",
Expand All @@ -77,7 +77,7 @@
"rollup": "^3.29.5",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^3.1.3",
"zod": "^3.24.1",
"zod": "3.25.0-beta.20250516T044623",
"zx": "^7.2.2"
}
}
53 changes: 35 additions & 18 deletions drizzle-zod/src/column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,28 @@ import type {
SingleStoreYear,
} from 'drizzle-orm/singlestore-core';
import type { SQLiteInteger, SQLiteReal, SQLiteText } from 'drizzle-orm/sqlite-core';
import { z } from 'zod';
import { z as zod } from 'zod';
import { z } from 'zod/v4';
import { z as zod } from 'zod/v4';
import { CONSTANTS } from './constants.ts';
import type { CreateSchemaFactoryOptions } from './schema.types.ts';
import { isColumnType, isWithEnum } from './utils.ts';
import type { Json } from './utils.ts';

export const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
export const jsonSchema: z.ZodType<Json> = z.union([literalSchema, z.record(z.any()), z.array(z.any())]);
export const jsonSchema: z.ZodType<Json> = z.union([literalSchema, z.record(z.string(), z.any()), z.array(z.any())]);
export const bufferSchema: z.ZodType<Buffer> = z.custom<Buffer>((v) => v instanceof Buffer); // eslint-disable-line no-instanceof/no-instanceof

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

if (isWithEnum(column)) {
schema = column.enumValues.length ? z.enum(column.enumValues) : z.string();
Expand All @@ -94,7 +101,7 @@ export function columnToSchema(column: Column, factory: CreateSchemaFactoryOptio
});
} // Handle other types
else if (isColumnType<PgArray<any, any>>(column, ['PgArray'])) {
schema = z.array(columnToSchema(column.baseColumn, z));
schema = z.array(columnToSchema(column.baseColumn, factory));
schema = column.size ? (schema as z.ZodArray<any>).length(column.size) : schema;
} else if (column.dataType === 'array') {
schema = z.array(z.any());
Expand Down Expand Up @@ -127,8 +134,10 @@ export function columnToSchema(column: Column, factory: CreateSchemaFactoryOptio
function numberColumnToSchema(
column: Column,
z: typeof zod,
coerce: CreateSchemaFactoryOptions['coerce'],
): z.ZodTypeAny {
coerce: CreateSchemaFactoryOptions<
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
>['coerce'],
): z.ZodType {
let unsigned = column.getSQLType().includes('unsigned');
let min!: number;
let max!: number;
Expand Down Expand Up @@ -228,31 +237,39 @@ function numberColumnToSchema(
max = Number.MAX_SAFE_INTEGER;
}

let schema = coerce === true || coerce?.number ? z.coerce.number() : z.number();
schema = schema.min(min).max(max);
return integer ? schema.int() : schema;
let schema = coerce === true || coerce?.number
? integer ? z.coerce.number() : z.coerce.number().int()
: integer
? z.int()
: z.number();
schema = schema.gte(min).lte(max);
return schema;
}

function bigintColumnToSchema(
column: Column,
z: typeof zod,
coerce: CreateSchemaFactoryOptions['coerce'],
): z.ZodTypeAny {
coerce: CreateSchemaFactoryOptions<
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
>['coerce'],
): z.ZodType {
const unsigned = column.getSQLType().includes('unsigned');
const min = unsigned ? 0n : CONSTANTS.INT64_MIN;
const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX;

const schema = coerce === true || coerce?.bigint ? z.coerce.bigint() : z.bigint();
return schema.min(min).max(max);
return schema.gte(min).lte(max);
}

function stringColumnToSchema(
column: Column,
z: typeof zod,
coerce: CreateSchemaFactoryOptions['coerce'],
): z.ZodTypeAny {
coerce: CreateSchemaFactoryOptions<
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
>['coerce'],
): z.ZodType {
if (isColumnType<PgUUID<ColumnBaseConfig<'string', 'PgUUID'>>>(column, ['PgUUID'])) {
return z.string().uuid();
return z.uuid();
}

let max: number | undefined;
Expand Down
85 changes: 63 additions & 22 deletions drizzle-zod/src/column.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Assume, Column } from 'drizzle-orm';
import type { z } from 'zod';
import type { z } from 'zod/v4';
import type { IsEnumDefined, IsNever, Json } from './utils.ts';

type HasBaseColumn<TColumn> = TColumn extends { _: { baseColumn: Column | undefined } }
Expand All @@ -9,54 +9,95 @@ type HasBaseColumn<TColumn> = TColumn extends { _: { baseColumn: Column | undefi

export type GetZodType<
TColumn extends Column,
TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
> = HasBaseColumn<TColumn> extends true ? z.ZodArray<
GetZodType<Assume<TColumn['_']['baseColumn'], Column>>
GetZodType<Assume<TColumn['_']['baseColumn'], Column>, TCoerce>
>
: TColumn['_']['columnType'] extends 'PgUUID' ? z.ZodUUID
: IsEnumDefined<TColumn['_']['enumValues']> extends true
? z.ZodEnum<Assume<TColumn['_']['enumValues'], [string, ...string[]]>>
: TColumn['_']['columnType'] extends 'PgGeometry' | 'PgPointTuple' ? z.ZodTuple<[z.ZodNumber, z.ZodNumber]>
: TColumn['_']['columnType'] extends 'PgLine' ? z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber]>
: TColumn['_']['data'] extends Date ? z.ZodDate
? z.ZodEnum<{ [K in Assume<TColumn['_']['enumValues'], [string, ...string[]]>[number]]: K }>
: TColumn['_']['columnType'] extends 'PgGeometry' | 'PgPointTuple' ? z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>
: TColumn['_']['columnType'] extends 'PgLine' ? z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>
: TColumn['_']['data'] extends Date ? CanCoerce<TCoerce, 'date'> extends true ? z.coerce.ZodCoercedDate : z.ZodDate
: TColumn['_']['data'] extends Buffer ? z.ZodType<Buffer>
: TColumn['_']['dataType'] extends 'array'
? z.ZodArray<GetZodPrimitiveType<Assume<TColumn['_']['data'], any[]>[number]>>
? z.ZodArray<GetZodPrimitiveType<Assume<TColumn['_']['data'], any[]>[number], '', TCoerce>>
: TColumn['_']['data'] extends Record<string, any>
? TColumn['_']['columnType'] extends
'PgJson' | 'PgJsonb' | 'MySqlJson' | 'SingleStoreJson' | 'SQLiteTextJson' | 'SQLiteBlobJson'
? z.ZodType<TColumn['_']['data'], z.ZodTypeDef, TColumn['_']['data']>
: z.ZodObject<{ [K in keyof TColumn['_']['data']]: GetZodPrimitiveType<TColumn['_']['data'][K]> }, 'strip'>
? z.ZodType<TColumn['_']['data'], TColumn['_']['data']>
: z.ZodObject<
{ [K in keyof TColumn['_']['data']]: GetZodPrimitiveType<TColumn['_']['data'][K], '', TCoerce> },
{},
{}
>
: TColumn['_']['dataType'] extends 'json' ? z.ZodType<Json>
: GetZodPrimitiveType<TColumn['_']['data']>;
: GetZodPrimitiveType<TColumn['_']['data'], TColumn['_']['columnType'], TCoerce>;

type GetZodPrimitiveType<TData> = TData extends number ? z.ZodNumber
: TData extends bigint ? z.ZodBigInt
: TData extends boolean ? z.ZodBoolean
: TData extends string ? z.ZodString
: z.ZodTypeAny;
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;

type GetZodPrimitiveType<
TData,
TColumnType,
TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
> = TColumnType extends
| 'MySqlTinyInt'
Comment thread
L-Mario564 marked this conversation as resolved.
| 'SingleStoreTinyInt'
| 'PgSmallInt'
| 'PgSmallSerial'
| 'MySqlSmallInt'
| 'MySqlMediumInt'
| 'SingleStoreSmallInt'
| 'SingleStoreMediumInt'
| 'PgInteger'
| 'PgSerial'
| 'MySqlInt'
| 'SingleStoreInt'
| 'PgBigInt53'
| 'PgBigSerial53'
| 'MySqlBigInt53'
| 'MySqlSerial'
| 'SingleStoreBigInt53'
| 'SingleStoreSerial'
| 'SQLiteInteger'
| 'MySqlYear'
| 'SingleStoreYear' ? CanCoerce<TCoerce, 'number'> extends true ? z.coerce.ZodCoercedNumber : z.ZodInt
: TData extends number ? CanCoerce<TCoerce, 'number'> extends true ? z.coerce.ZodCoercedNumber : z.ZodNumber
: TData extends bigint ? CanCoerce<TCoerce, 'bigint'> extends true ? z.coerce.ZodCoercedBigInt : z.ZodBigInt
: TData extends boolean ? CanCoerce<TCoerce, 'boolean'> extends true ? z.coerce.ZodCoercedBoolean : z.ZodBoolean
: TData extends string ? CanCoerce<TCoerce, 'string'> extends true ? z.coerce.ZodCoercedString : z.ZodString
: z.ZodType;

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

type HandleInsertColumn<
TSchema extends z.ZodTypeAny,
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.ZodTypeAny,
TSchema extends z.ZodType,
TColumn extends Column,
> = TColumn['_']['notNull'] extends true ? z.ZodOptional<TSchema>
: z.ZodOptional<z.ZodNullable<TSchema>>;

export type HandleColumn<
TType extends 'select' | 'insert' | 'update',
TColumn extends Column,
> = TType extends 'select' ? HandleSelectColumn<GetZodType<TColumn>, TColumn>
: TType extends 'insert' ? HandleInsertColumn<GetZodType<TColumn>, TColumn>
: TType extends 'update' ? HandleUpdateColumn<GetZodType<TColumn>, TColumn>
: GetZodType<TColumn>;
TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
> = TType extends 'select' ? HandleSelectColumn<GetZodType<TColumn, TCoerce>, TColumn>
: TType extends 'insert' ? HandleInsertColumn<GetZodType<TColumn, TCoerce>, TColumn>
: TType extends 'update' ? HandleUpdateColumn<GetZodType<TColumn, TCoerce>, TColumn>
: GetZodType<TColumn, TCoerce>;
33 changes: 21 additions & 12 deletions drizzle-zod/src/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Column, getTableColumns, getViewSelectedFields, is, isTable, isView, SQL } from 'drizzle-orm';
import type { Table, View } from 'drizzle-orm';
import type { PgEnum } from 'drizzle-orm/pg-core';
import { z } from 'zod';
import { z } from 'zod/v4';
import { columnToSchema } from './column.ts';
import type { Conditions } from './schema.types.internal.ts';
import type {
Expand All @@ -20,9 +20,11 @@ function handleColumns(
columns: Record<string, any>,
refinements: Record<string, any>,
conditions: Conditions,
factory?: CreateSchemaFactoryOptions,
): z.ZodTypeAny {
const columnSchemas: Record<string, z.ZodTypeAny> = {};
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') {
Expand Down Expand Up @@ -61,7 +63,12 @@ function handleColumns(
return z.object(columnSchemas) as any;
}

function handleEnum(enum_: PgEnum<any>, factory?: CreateSchemaFactoryOptions) {
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);
}
Expand All @@ -84,7 +91,7 @@ const updateConditions: Conditions = {
nullable: (column) => !column.notNull,
};

export const createSelectSchema: CreateSelectSchema = (
export const createSelectSchema: CreateSelectSchema<undefined> = (
entity: Table | View | PgEnum<[string, ...string[]]>,
refine?: Record<string, any>,
) => {
Expand All @@ -95,24 +102,26 @@ export const createSelectSchema: CreateSelectSchema = (
return handleColumns(columns, refine ?? {}, selectConditions) as any;
};

export const createInsertSchema: CreateInsertSchema = (
export const createInsertSchema: CreateInsertSchema<undefined> = (
entity: Table,
refine?: Record<string, any>,
) => {
const columns = getColumns(entity);
return handleColumns(columns, refine ?? {}, insertConditions) as any;
};

export const createUpdateSchema: CreateUpdateSchema = (
export const createUpdateSchema: CreateUpdateSchema<undefined> = (
entity: Table,
refine?: Record<string, any>,
) => {
const columns = getColumns(entity);
return handleColumns(columns, refine ?? {}, updateConditions) as any;
};

export function createSchemaFactory(options?: CreateSchemaFactoryOptions) {
const createSelectSchema: CreateSelectSchema = (
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>,
) => {
Expand All @@ -123,15 +132,15 @@ export function createSchemaFactory(options?: CreateSchemaFactoryOptions) {
return handleColumns(columns, refine ?? {}, selectConditions, options) as any;
};

const createInsertSchema: CreateInsertSchema = (
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 = (
const createUpdateSchema: CreateUpdateSchema<TCoerce> = (
entity: Table,
refine?: Record<string, any>,
) => {
Expand Down
Loading