Skip to content

Commit 0b8920b

Browse files
Support Zod v4 in drizzle-zod (#4478)
* Optimize drizzle-arktype types * More drizzle-arktype type optimizations * Set new baselines for type benchmarks * Change Buffer schema definition for drizzle-arktype * Optimize drizzle-zod types * Optimize drizzle-valibot types * Optimize drizzle-typebox types * Update drizzle-zod to use Zod v4 * Update imports --------- Co-authored-by: Andrii Sherman <andreysherman11@gmail.com>
1 parent 08944da commit 0b8920b

13 files changed

Lines changed: 414 additions & 332 deletions

drizzle-zod/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"license": "Apache-2.0",
6666
"peerDependencies": {
6767
"drizzle-orm": ">=0.36.0",
68-
"zod": ">=3.0.0"
68+
"zod": "^3.25.0"
6969
},
7070
"devDependencies": {
7171
"@rollup/plugin-typescript": "^11.1.0",
@@ -77,7 +77,7 @@
7777
"rollup": "^3.29.5",
7878
"vite-tsconfig-paths": "^4.3.2",
7979
"vitest": "^3.1.3",
80-
"zod": "^3.24.1",
80+
"zod": "3.25.0-beta.20250516T044623",
8181
"zx": "^7.2.2"
8282
}
8383
}

drizzle-zod/src/column.ts

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,28 @@ import type {
5353
SingleStoreYear,
5454
} from 'drizzle-orm/singlestore-core';
5555
import type { SQLiteInteger, SQLiteReal, SQLiteText } from 'drizzle-orm/sqlite-core';
56-
import { z } from 'zod';
57-
import { z as zod } from 'zod';
56+
import { z } from 'zod/v4';
57+
import { z as zod } from 'zod/v4';
5858
import { CONSTANTS } from './constants.ts';
5959
import type { CreateSchemaFactoryOptions } from './schema.types.ts';
6060
import { isColumnType, isWithEnum } from './utils.ts';
6161
import type { Json } from './utils.ts';
6262

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

67-
export function columnToSchema(column: Column, factory: CreateSchemaFactoryOptions | undefined): z.ZodTypeAny {
68-
const z = factory?.zodInstance ?? zod;
67+
export function columnToSchema(
68+
column: Column,
69+
factory:
70+
| CreateSchemaFactoryOptions<
71+
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
72+
>
73+
| undefined,
74+
): z.ZodType {
75+
const z: typeof zod = factory?.zodInstance ?? zod;
6976
const coerce = factory?.coerce ?? {};
70-
let schema!: z.ZodTypeAny;
77+
let schema!: z.ZodType;
7178

7279
if (isWithEnum(column)) {
7380
schema = column.enumValues.length ? z.enum(column.enumValues) : z.string();
@@ -94,7 +101,7 @@ export function columnToSchema(column: Column, factory: CreateSchemaFactoryOptio
94101
});
95102
} // Handle other types
96103
else if (isColumnType<PgArray<any, any>>(column, ['PgArray'])) {
97-
schema = z.array(columnToSchema(column.baseColumn, z));
104+
schema = z.array(columnToSchema(column.baseColumn, factory));
98105
schema = column.size ? (schema as z.ZodArray<any>).length(column.size) : schema;
99106
} else if (column.dataType === 'array') {
100107
schema = z.array(z.any());
@@ -127,8 +134,10 @@ export function columnToSchema(column: Column, factory: CreateSchemaFactoryOptio
127134
function numberColumnToSchema(
128135
column: Column,
129136
z: typeof zod,
130-
coerce: CreateSchemaFactoryOptions['coerce'],
131-
): z.ZodTypeAny {
137+
coerce: CreateSchemaFactoryOptions<
138+
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
139+
>['coerce'],
140+
): z.ZodType {
132141
let unsigned = column.getSQLType().includes('unsigned');
133142
let min!: number;
134143
let max!: number;
@@ -228,31 +237,39 @@ function numberColumnToSchema(
228237
max = Number.MAX_SAFE_INTEGER;
229238
}
230239

231-
let schema = coerce === true || coerce?.number ? z.coerce.number() : z.number();
232-
schema = schema.min(min).max(max);
233-
return integer ? schema.int() : schema;
240+
let schema = coerce === true || coerce?.number
241+
? integer ? z.coerce.number() : z.coerce.number().int()
242+
: integer
243+
? z.int()
244+
: z.number();
245+
schema = schema.gte(min).lte(max);
246+
return schema;
234247
}
235248

236249
function bigintColumnToSchema(
237250
column: Column,
238251
z: typeof zod,
239-
coerce: CreateSchemaFactoryOptions['coerce'],
240-
): z.ZodTypeAny {
252+
coerce: CreateSchemaFactoryOptions<
253+
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
254+
>['coerce'],
255+
): z.ZodType {
241256
const unsigned = column.getSQLType().includes('unsigned');
242257
const min = unsigned ? 0n : CONSTANTS.INT64_MIN;
243258
const max = unsigned ? CONSTANTS.INT64_UNSIGNED_MAX : CONSTANTS.INT64_MAX;
244259

245260
const schema = coerce === true || coerce?.bigint ? z.coerce.bigint() : z.bigint();
246-
return schema.min(min).max(max);
261+
return schema.gte(min).lte(max);
247262
}
248263

249264
function stringColumnToSchema(
250265
column: Column,
251266
z: typeof zod,
252-
coerce: CreateSchemaFactoryOptions['coerce'],
253-
): z.ZodTypeAny {
267+
coerce: CreateSchemaFactoryOptions<
268+
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
269+
>['coerce'],
270+
): z.ZodType {
254271
if (isColumnType<PgUUID<ColumnBaseConfig<'string', 'PgUUID'>>>(column, ['PgUUID'])) {
255-
return z.string().uuid();
272+
return z.uuid();
256273
}
257274

258275
let max: number | undefined;

drizzle-zod/src/column.types.ts

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Assume, Column } from 'drizzle-orm';
2-
import type { z } from 'zod';
2+
import type { z } from 'zod/v4';
33
import type { IsEnumDefined, IsNever, Json } from './utils.ts';
44

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

1010
export type GetZodType<
1111
TColumn extends Column,
12+
TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
1213
> = HasBaseColumn<TColumn> extends true ? z.ZodArray<
13-
GetZodType<Assume<TColumn['_']['baseColumn'], Column>>
14+
GetZodType<Assume<TColumn['_']['baseColumn'], Column>, TCoerce>
1415
>
16+
: TColumn['_']['columnType'] extends 'PgUUID' ? z.ZodUUID
1517
: IsEnumDefined<TColumn['_']['enumValues']> extends true
16-
? z.ZodEnum<Assume<TColumn['_']['enumValues'], [string, ...string[]]>>
17-
: TColumn['_']['columnType'] extends 'PgGeometry' | 'PgPointTuple' ? z.ZodTuple<[z.ZodNumber, z.ZodNumber]>
18-
: TColumn['_']['columnType'] extends 'PgLine' ? z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber]>
19-
: TColumn['_']['data'] extends Date ? z.ZodDate
18+
? z.ZodEnum<{ [K in Assume<TColumn['_']['enumValues'], [string, ...string[]]>[number]]: K }>
19+
: TColumn['_']['columnType'] extends 'PgGeometry' | 'PgPointTuple' ? z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>
20+
: TColumn['_']['columnType'] extends 'PgLine' ? z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>
21+
: TColumn['_']['data'] extends Date ? CanCoerce<TCoerce, 'date'> extends true ? z.coerce.ZodCoercedDate : z.ZodDate
2022
: TColumn['_']['data'] extends Buffer ? z.ZodType<Buffer>
2123
: TColumn['_']['dataType'] extends 'array'
22-
? z.ZodArray<GetZodPrimitiveType<Assume<TColumn['_']['data'], any[]>[number]>>
24+
? z.ZodArray<GetZodPrimitiveType<Assume<TColumn['_']['data'], any[]>[number], '', TCoerce>>
2325
: TColumn['_']['data'] extends Record<string, any>
2426
? TColumn['_']['columnType'] extends
2527
'PgJson' | 'PgJsonb' | 'MySqlJson' | 'SingleStoreJson' | 'SQLiteTextJson' | 'SQLiteBlobJson'
26-
? z.ZodType<TColumn['_']['data'], z.ZodTypeDef, TColumn['_']['data']>
27-
: z.ZodObject<{ [K in keyof TColumn['_']['data']]: GetZodPrimitiveType<TColumn['_']['data'][K]> }, 'strip'>
28+
? z.ZodType<TColumn['_']['data'], TColumn['_']['data']>
29+
: z.ZodObject<
30+
{ [K in keyof TColumn['_']['data']]: GetZodPrimitiveType<TColumn['_']['data'][K], '', TCoerce> },
31+
{},
32+
{}
33+
>
2834
: TColumn['_']['dataType'] extends 'json' ? z.ZodType<Json>
29-
: GetZodPrimitiveType<TColumn['_']['data']>;
35+
: GetZodPrimitiveType<TColumn['_']['data'], TColumn['_']['columnType'], TCoerce>;
3036

31-
type GetZodPrimitiveType<TData> = TData extends number ? z.ZodNumber
32-
: TData extends bigint ? z.ZodBigInt
33-
: TData extends boolean ? z.ZodBoolean
34-
: TData extends string ? z.ZodString
35-
: z.ZodTypeAny;
37+
type CanCoerce<
38+
TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
39+
TTo extends 'bigint' | 'boolean' | 'date' | 'number' | 'string',
40+
> = TCoerce extends true ? true
41+
: TCoerce extends Record<string, any> ? TCoerce[TTo] extends true ? true
42+
: false
43+
: false;
44+
45+
type GetZodPrimitiveType<
46+
TData,
47+
TColumnType,
48+
TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
49+
> = TColumnType extends
50+
| 'MySqlTinyInt'
51+
| 'SingleStoreTinyInt'
52+
| 'PgSmallInt'
53+
| 'PgSmallSerial'
54+
| 'MySqlSmallInt'
55+
| 'MySqlMediumInt'
56+
| 'SingleStoreSmallInt'
57+
| 'SingleStoreMediumInt'
58+
| 'PgInteger'
59+
| 'PgSerial'
60+
| 'MySqlInt'
61+
| 'SingleStoreInt'
62+
| 'PgBigInt53'
63+
| 'PgBigSerial53'
64+
| 'MySqlBigInt53'
65+
| 'MySqlSerial'
66+
| 'SingleStoreBigInt53'
67+
| 'SingleStoreSerial'
68+
| 'SQLiteInteger'
69+
| 'MySqlYear'
70+
| 'SingleStoreYear' ? CanCoerce<TCoerce, 'number'> extends true ? z.coerce.ZodCoercedNumber : z.ZodInt
71+
: TData extends number ? CanCoerce<TCoerce, 'number'> extends true ? z.coerce.ZodCoercedNumber : z.ZodNumber
72+
: TData extends bigint ? CanCoerce<TCoerce, 'bigint'> extends true ? z.coerce.ZodCoercedBigInt : z.ZodBigInt
73+
: TData extends boolean ? CanCoerce<TCoerce, 'boolean'> extends true ? z.coerce.ZodCoercedBoolean : z.ZodBoolean
74+
: TData extends string ? CanCoerce<TCoerce, 'string'> extends true ? z.coerce.ZodCoercedString : z.ZodString
75+
: z.ZodType;
3676

3777
type HandleSelectColumn<
38-
TSchema extends z.ZodTypeAny,
78+
TSchema extends z.ZodType,
3979
TColumn extends Column,
4080
> = TColumn['_']['notNull'] extends true ? TSchema
4181
: z.ZodNullable<TSchema>;
4282

4383
type HandleInsertColumn<
44-
TSchema extends z.ZodTypeAny,
84+
TSchema extends z.ZodType,
4585
TColumn extends Column,
4686
> = TColumn['_']['notNull'] extends true ? TColumn['_']['hasDefault'] extends true ? z.ZodOptional<TSchema>
4787
: TSchema
4888
: z.ZodOptional<z.ZodNullable<TSchema>>;
4989

5090
type HandleUpdateColumn<
51-
TSchema extends z.ZodTypeAny,
91+
TSchema extends z.ZodType,
5292
TColumn extends Column,
5393
> = TColumn['_']['notNull'] extends true ? z.ZodOptional<TSchema>
5494
: z.ZodOptional<z.ZodNullable<TSchema>>;
5595

5696
export type HandleColumn<
5797
TType extends 'select' | 'insert' | 'update',
5898
TColumn extends Column,
59-
> = TType extends 'select' ? HandleSelectColumn<GetZodType<TColumn>, TColumn>
60-
: TType extends 'insert' ? HandleInsertColumn<GetZodType<TColumn>, TColumn>
61-
: TType extends 'update' ? HandleUpdateColumn<GetZodType<TColumn>, TColumn>
62-
: GetZodType<TColumn>;
99+
TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
100+
> = TType extends 'select' ? HandleSelectColumn<GetZodType<TColumn, TCoerce>, TColumn>
101+
: TType extends 'insert' ? HandleInsertColumn<GetZodType<TColumn, TCoerce>, TColumn>
102+
: TType extends 'update' ? HandleUpdateColumn<GetZodType<TColumn, TCoerce>, TColumn>
103+
: GetZodType<TColumn, TCoerce>;

drizzle-zod/src/schema.ts

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Column, getTableColumns, getViewSelectedFields, is, isTable, isView, SQL } from 'drizzle-orm';
22
import type { Table, View } from 'drizzle-orm';
33
import type { PgEnum } from 'drizzle-orm/pg-core';
4-
import { z } from 'zod';
4+
import { z } from 'zod/v4';
55
import { columnToSchema } from './column.ts';
66
import type { Conditions } from './schema.types.internal.ts';
77
import type {
@@ -20,9 +20,11 @@ function handleColumns(
2020
columns: Record<string, any>,
2121
refinements: Record<string, any>,
2222
conditions: Conditions,
23-
factory?: CreateSchemaFactoryOptions,
24-
): z.ZodTypeAny {
25-
const columnSchemas: Record<string, z.ZodTypeAny> = {};
23+
factory?: CreateSchemaFactoryOptions<
24+
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
25+
>,
26+
): z.ZodType {
27+
const columnSchemas: Record<string, z.ZodType> = {};
2628

2729
for (const [key, selected] of Object.entries(columns)) {
2830
if (!is(selected, Column) && !is(selected, SQL) && !is(selected, SQL.Aliased) && typeof selected === 'object') {
@@ -61,7 +63,12 @@ function handleColumns(
6163
return z.object(columnSchemas) as any;
6264
}
6365

64-
function handleEnum(enum_: PgEnum<any>, factory?: CreateSchemaFactoryOptions) {
66+
function handleEnum(
67+
enum_: PgEnum<any>,
68+
factory?: CreateSchemaFactoryOptions<
69+
Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined
70+
>,
71+
) {
6572
const zod: typeof z = factory?.zodInstance ?? z;
6673
return zod.enum(enum_.enumValues);
6774
}
@@ -84,7 +91,7 @@ const updateConditions: Conditions = {
8491
nullable: (column) => !column.notNull,
8592
};
8693

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

98-
export const createInsertSchema: CreateInsertSchema = (
105+
export const createInsertSchema: CreateInsertSchema<undefined> = (
99106
entity: Table,
100107
refine?: Record<string, any>,
101108
) => {
102109
const columns = getColumns(entity);
103110
return handleColumns(columns, refine ?? {}, insertConditions) as any;
104111
};
105112

106-
export const createUpdateSchema: CreateUpdateSchema = (
113+
export const createUpdateSchema: CreateUpdateSchema<undefined> = (
107114
entity: Table,
108115
refine?: Record<string, any>,
109116
) => {
110117
const columns = getColumns(entity);
111118
return handleColumns(columns, refine ?? {}, updateConditions) as any;
112119
};
113120

114-
export function createSchemaFactory(options?: CreateSchemaFactoryOptions) {
115-
const createSelectSchema: CreateSelectSchema = (
121+
export function createSchemaFactory<
122+
TCoerce extends Partial<Record<'bigint' | 'boolean' | 'date' | 'number' | 'string', true>> | true | undefined,
123+
>(options?: CreateSchemaFactoryOptions<TCoerce>) {
124+
const createSelectSchema: CreateSelectSchema<TCoerce> = (
116125
entity: Table | View | PgEnum<[string, ...string[]]>,
117126
refine?: Record<string, any>,
118127
) => {
@@ -123,15 +132,15 @@ export function createSchemaFactory(options?: CreateSchemaFactoryOptions) {
123132
return handleColumns(columns, refine ?? {}, selectConditions, options) as any;
124133
};
125134

126-
const createInsertSchema: CreateInsertSchema = (
135+
const createInsertSchema: CreateInsertSchema<TCoerce> = (
127136
entity: Table,
128137
refine?: Record<string, any>,
129138
) => {
130139
const columns = getColumns(entity);
131140
return handleColumns(columns, refine ?? {}, insertConditions, options) as any;
132141
};
133142

134-
const createUpdateSchema: CreateUpdateSchema = (
143+
const createUpdateSchema: CreateUpdateSchema<TCoerce> = (
135144
entity: Table,
136145
refine?: Record<string, any>,
137146
) => {

0 commit comments

Comments
 (0)