drizzle-zod-codegen
v0.2.0
Published
Generate static Zod schema files from Drizzle ORM schemas
Maintainers
Readme
drizzle-zod-codegen
Generate static Zod schema files from Drizzle ORM schemas.
Why?
✅ Improved parity with drizzle-zod: Uses the same mapping logic and serializes most Zod runtime constructs
✅ Complete type coverage: All column types, enums, views
✅ Expanded Zod coverage: Many first-class Zod constructs are now supported (discriminated unions, intersections, pipelines, readonly, symbol, NaN, and static .catch() fallbacks). Native enums with numeric keys are preserved as object-literal enums.
- Inspectable: Easy to see exactly what schemas you have
- Version controllable: Track changes in git
Extra Zod constructs supported
Discriminated unions (
z.discriminatedUnion) — emitted usingz.discriminatedUnionwhen a discriminator is present Intersections (z.intersection) — emitted usingz.intersectionPipelines and transforms (z.pipe) — emitted where possible as.pipe()when both sides are serializable Readonly objects (.readonly()) — emitted using.readonly()where applicablez.symbol(),z.nan()and objectz.catch()fallbacks (static values only) are serializable - Better tree-shaking: Only import what you need
Serializer guarantees and behavior
- Static defaults and fallbacks are serialized when they are deterministic values:
- Example:
.default('hello'),.catch('fallback')will be emitted as.default('hello')and.catch('fallback')
- Example:
- Dynamic factories used as defaults or catch fallbacks (e.g.,
.default(() => Math.random()),.catch(() => computeDefault())) cannot be serialized statically and will cause the generator to throw an error. Replace with a deterministic default or post-process the generated schema to integrate runtime factories. - For safety and to avoid silently permissive schemas, the serializer now throws for unsupported Zod constructs (for example,
z.function()), rather than falling back toz.any(). This prevents subtle correctness issues and ensures generated schemas don't become lax silently. - Runtime-only refinements and predicates such as
.refine(),.superRefine(), unsupportedz.custom(...), and other effect-like closures are not serialized. The generator throws instead of silently weakening the schema.
Features
- Unsupported Zod constructs or dynamic factories – If the generator throws with messages like
Unsupported Zod typeorZodDefault uses a dynamic factory, this means your schema uses a runtime-only construct (for example,.default(() => Math.random())orz.function()). Options:- Replace dynamic factories with static defaults or move the dynamic factory to runtime code and keep the generated schema static.
- Post-process the generated
.zod.tsto add the runtime-specific refinements or custom functions manually. - Write a small wrapper that extends the generated schema with
.refine()/.transform()yourself.
- ✅ Full parity with drizzle-zod: Uses the same mapping logic
- ✅ All dialects supported: PostgreSQL, MySQL, SQLite, SingleStore
- ✅ Complete type coverage: All column types, enums, views
- ✅ Smart schema generation:
SelectSchema- for query resultsInsertSchema- for inserts (excludes generated columns, makes defaults optional)UpdateSchema- for updates (all fields optional)
- ✅ Flexible discovery: Config file, directory scanning, or explicit paths
- ✅ CLI and programmatic API
Installation
npm install -D drizzle-zod-codegen
# or
pnpm add -D drizzle-zod-codegenUsage
CLI
# Generate from specific files
drizzle-zod-codegen generate src/schema.ts
# Scan a directory
drizzle-zod-codegen generate --dir src/db
# Use drizzle.config.ts/js/mts/mjs
drizzle-zod-codegen generate --config drizzle.config.ts
# Custom pattern
drizzle-zod-codegen generate --dir src --pattern "**/*.model.ts"
# Emit every schema into a single bundle file
drizzle-zod-codegen generate src/**/*.schema.ts --output single --out ./schemas/all.zod.ts
# Emit one file per Drizzle entity
drizzle-zod-codegen generate src/schema.ts --output per-schema --out-dir ./generated-schemas
# Load explicit custom-type mappings and per-entity refinements
drizzle-zod-codegen generate src/schema.ts --codegen-config ./drizzle-zod-codegen.config.ts
#### Additional Flags
- `--no-cache` – force `bundle-require` to rebundle each schema/config file instead of reusing a module cache entry, helpful when you run the generator repeatedly in the same process (watch mode or a scripted loop).Output Modes
per-file(default) – writes one.zodfile next to each schema (or mirrors into--out-dir).per-schema– writes one file per table/view/enum named{Entity}.zod.ts. Combine with--out-dirto collect them.single– aggregates every generated schema into the file provided via--out.
Programmatic API
import { generateZodSchemas, discoverSchemaFiles, runGenerateCommand } from 'drizzle-zod-codegen';
// Discover schema files
const schemaFiles = await discoverSchemaFiles({
scanDir: 'src/db',
pattern: '**/*.schema.ts',
});
// Generate for each file
for (const file of schemaFiles) {
await generateZodSchemas({
inputPath: file.path,
outputPath: file.outputPath,
});
}
// Or aggregate everything into a single file via runGenerateCommand
await runGenerateCommand({
files: schemaFiles.map(file => file.path),
mode: 'single',
outFile: './all-schemas.zod.ts',
});Pass forceRefreshModuleCache: true to generateZodSchemas() if you want to clear the loader cache before each invocation (useful for watch scripts that regenerate files without restarting the node process).
Custom SQL Types
drizzle-zod maps customType() columns to z.any() at runtime because Drizzle does not retain the TypeScript generic for arbitrary custom types at runtime. drizzle-zod-codegen now handles this in two explicit ways:
- Built-in mapping for PostgreSQL
citext→z.string() - Optional
customTypesconfig for any other SQL type name exposed bycolumn.getSQLType()
Example config:
import { z } from 'zod';
export default {
customTypes: {
currency_code: z.string().length(3),
},
entities: {
users: {
insert: {
// Any explicit drizzle-zod refinement tree still works here.
},
},
},
};When custom SQL types are mapped through this config, the generator uses drizzle-zod's own refinement hook so select/insert/update nullability and optionality still come from upstream logic.
TypeScript Support
Schema and config files are bundled through esbuild at runtime, so TypeScript enums, satisfies, const assertions, and other non-JavaScript syntax work without extra loaders.
Example
Given this Drizzle schema:
// users.schema.ts
import { pgTable, serial, text, integer, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull(),
age: integer('age'),
createdAt: timestamp('created_at').notNull().defaultNow(),
});Running drizzle-zod-codegen generate users.schema.ts generates:
// users.zod.ts
import { z } from 'zod';
export const UsersSelectSchema = z.object({
id: z.number().int().gte(-2147483648).lte(2147483647),
name: z.string(),
email: z.string(),
age: z.number().int().gte(-2147483648).lte(2147483647).nullable(),
createdAt: z.date()
});
export const UsersInsertSchema = z.object({
id: z.number().int().gte(-2147483648).lte(2147483647).optional(),
name: z.string(),
email: z.string(),
age: z.number().int().gte(-2147483648).lte(2147483647).nullable().optional(),
createdAt: z.date().optional()
});
export const UsersUpdateSchema = z.object({
id: z.number().int().gte(-2147483648).lte(2147483647).optional(),
name: z.string().optional(),
email: z.string().optional(),
age: z.number().int().gte(-2147483648).lte(2147483647).nullable().optional(),
createdAt: z.date().optional()
});Output Naming
- Tables:
{PascalCase}SelectSchema,{PascalCase}InsertSchema,{PascalCase}UpdateSchema - Views:
{PascalCase}SelectSchema - Enums:
{PascalCase}Schema
Supported Features
Column Types
- All numeric types (with proper min/max validation)
- Strings (with length constraints)
- Dates & timestamps
- Booleans
- JSON & JSONB
- Arrays
- UUIDs
- Enums
- Geometry types (PostGIS)
- Buffers
- Custom SQL types via built-in mappings or
--codegen-config
Schema Features
- Primary keys
- Not null constraints
- Default values
- Generated columns (excluded from insert/update)
- Nullable columns
- Views (including nested selections)
- PostgreSQL enums
Discovery Options
1. Explicit Files (CLI)
drizzle-zod-codegen generate src/users.schema.ts src/posts.schema.ts2. Directory Scanning
drizzle-zod-codegen generate --dir src/db --pattern "**/*.schema.ts"Default pattern: **/*.schema.{ts,js,mts,mjs,cts,cjs}
3. Drizzle Config
// drizzle.config.ts
export default {
schema: './src/db/schema.ts',
// or multiple files
schema: ['./src/db/users.ts', './src/db/posts.ts'],
// or glob pattern
schema: './src/db/**/*.schema.ts',
};Then run:
drizzle-zod-codegen generateTroubleshooting
- Aggregate export errors – When serialization fails for a table, view, or enum export the generator throws an
AggregateError; the CLI printsOne or more exports failed to process:followed by each export's message so you can fix the offending export before retrying. - Schema cache misbehavior – Include the
--no-cacheflag when you run the generator repeatedly in the same process to forcebundle-requireto rebundle every schema/config file instead of reusing cached modules.
Output Files
By default, generates one .zod.ts file per schema file:
src/db/schema.ts→src/db/schema.zod.tsusers.schema.ts→users.zod.ts
Comparison with drizzle-zod
| Feature | drizzle-zod | drizzle-zod-codegen | |---------|------------|---------------------| | Schema generation | Runtime | Static files | | Type coverage | ✅ Complete | ✅ Complete | | All dialects | ✅ | ✅ | | Refinements | ✅ | ⏳ Planned | | Custom Zod instance | ✅ | ⏳ Planned | | Coercion | ✅ | ⏳ Planned | | File size | Small | Larger (explicit schemas) | | Inspectable | ❌ | ✅ | | Customizable output | ❌ | ✅ | | Build step required | ❌ | ✅ |
License
MIT
Credits
Built using the same column mapping logic as drizzle-zod to ensure 100% compatibility.
