drizzle-team / drizzle-team/drizzle-orm
feat(drizzle-orm): Add $validator() for Standard Schema integration on columns
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
### Background: Standard Schema Changes Everything
[Standard Schema](https://standardschema.dev/) is now a mature v1 specification implemented by all major TypeScript validation libraries:
- Zod (v3.23+)
- Valibot (v1+)
- ArkType
- Effect Schema
- TypeBox
This provides a **library-agnostic interface** for schema validation that didn't exist when similar features were previously requested. It enables a clean solution that avoids the concerns raised in past discussions.
---
### Prior Art & Related Issues
This request builds on recurring community demand:
| Issue | Status | Request |
|-------|--------|---------|
| #3565 | Open | `$inferTypeFromSchema()` for JSON columns and custom types |
| #4303 | Open | Standard Schema integration with Drizzle |
| #4810 | Open | Common refinements across create/insert/update schemas |
| #4530 | Open | `.describe()` metadata on columns |
| #4863 | **Closed** | Zod refinements on table definitions |
**Regarding #4863**: It was closed with the concern that it would "mess drizzle-orm database schema approach with validator package schema approach."
**Standard Schema directly addresses this concern:**
1. **Library-agnostic** — `$validator()` accepts any `StandardSchemaV1`, not Zod/Valibot-specific types
2. **Opaque storage** — drizzle-orm just stores the schema reference as metadata; no validation methods added to columns
3. **Integration in validator packages** — the "mixing" happens in `drizzle-zod`/`drizzle-valibot`, not drizzle-orm core
4. **Consistent with existing patterns** — `$type()` already stores type metadata; `$validator()` adds a runtime reference
---
### Problem
When working with JSON columns or adding domain-specific validation, developers face redundant definitions:
```ts
// 1. Define the schema
const UserSettingsSchema = z.object({
theme: z.enum(['light', 'dark']),
notifications: z.boolean(),
fontSize: z.number().min(8).max(32),
});
// 2. Extract type and pass to $type
type UserSettings = z.infer;
const users = pgTable('users', {
id: serial('id').primaryKey(),
settings: json('settings').$type().notNull(),
email: varchar('email', { length: 255 }).notNull(),
});
// 3. Pass the SAME schema again as refinement
const insertUserSchema = createInsertSchema(users, {
settings: UserSettingsSchema, // Redundant!
});
```
**Pain points:**
- Schema defined twice (column + refinement)
- Manual type extraction with `z.infer<>` + `$type`
- Easy type/schema mismatches
- No single source of truth
---
### Proposed Solution
Add a `$validator()` method that accepts any Standard Schema compliant validator:
```ts
const users = pgTable('users', {
id: serial('id').primaryKey(),
settings: json('settings').$validator(UserSettingsSchema).notNull(),
email: varchar('email', { length: 255 }).notNull(),
age: integer('age').$validator(z.number().min(0).max(150)),
});
```
**Benefits:**
- **Type inference** — automatically infers TypeScript type (no manual `$type` needed)
- **Single source of truth** — schema defined once, used everywhere
- **Library agnostic** — works with Zod, Valibot, ArkType, Effect Schema, etc.
- **Backward compatible** — `$type` continues to work
---
### API
```ts
$validator(schema: TSchema): $Validator
```
**Type helper** (matches beta branch patterns):
```ts
export type $Validator = T & {
_: {
$type: StandardSchemaV1.InferOutput;
$validator: TSchema;
};
};
```
The schema is accessible via `column.validator` at runtime.
---
### Examples
**JSON columns with complex types:**
```ts
const OrderItemSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().positive(),
price: z.number().positive(),
});
const orders = pgTable('orders', {
id: serial('id').primaryKey(),
items: json('items').$validator(z.array(OrderItemSchema)).notNull(),
// TypeScript knows: items is { productId: string, quantity: number, price: number }[]
});
```
**Domain-specific validation on primitives:**
```ts
const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 }).$validator(z.string().email()),
age: integer('age').$validator(z.number().int().min(0).max(150)),
website: text('website').$validator(z.string().url().optional()),
});
```
**Works with any Standard Schema library:**
```ts
// Valibot
import * as v from 'valibot';
const users = pgTable('users', {
settings: json('settings').$validator(v.object({
theme: v.picklist(['light', 'dark']),
})),
});
// ArkType
import { type } from 'arktype';
const users = pgTable('users', {
settings: json('settings').$validator(type({
theme: "'light' | 'dark'",
})),
});
```
---
### Future Direction: `drizzle-zod` Integration
This feature enables `drizzle-zod` to automatically use attached validators:
**Today (with this feature):**
```ts
const users = pgTable('users', {
settings: json('settings').$validator(UserSettingsSchema).notNull(),
age: integer('age').$validator(z.number().min(0).max(150)),
});
// Still need refinements
const insertSchema = createInsertSchema(users, {
settings: UserSettingsSchema, // Redundant for now
});
```
**Future `drizzle-zod` enhancement:**
```ts
import { createInsertSchema } from 'drizzle-zod';
const insertSchema = createInsertSchema(users);
// Automatically uses $validator schemas!
// - settings: attached UserSettingsSchema (preserves all constraints)
// - email: generated z.string().max(255) from column type
// - age: attached z.number().min(0).max(150)
insertSchema.parse(data); // Native Zod DX
type InsertUser = z.infer;
```
**How it would work:**
- Check each column for `validator` property
- If vendor is `"zod"`: use directly (preserves `.min()`, `.email()`, etc.)
- If different vendor: wrap via `~standard.validate()`
- If not present: generate from column type (existing behavior)
**Refinements still work for overrides:**
```ts
const insertSchema = createInsertSchema(users, {
email: (s) => s.email(), // Enhance generated
age: z.number().min(18), // Override attached
});
```
---
### Implementation Notes (Beta Branch)
**Column Builder (`drizzle-orm/src/column-builder.ts`):**
```ts
import type { StandardSchemaV1 } from '@standard-schema/spec';
export type $Validator = T & {
_: {
$type: StandardSchemaV1.InferOutput;
$validator: TSchema;
};
};
// In ColumnBuilder class
$validator(schema: TSchema): $Validator {
(this.config as any).validator = schema;
return this as $Validator;
}
```
**Runtime config (`ColumnBuilderRuntimeConfig`):**
```ts
export interface ColumnBuilderRuntimeConfig {
// ... existing fields
validator: StandardSchemaV1 | undefined;
}
```
**Column (`drizzle-orm/src/column.ts`):**
```ts
// Expose similar to `length` in beta
readonly validator: StandardSchemaV1 | undefined = undefined;
// In constructor:
this.validator = config.validator;
```
---
### Why Standard Schema Solves Previous Concerns
| Concern from #4863 | How Standard Schema Addresses It |
|--------------------|----------------------------------|
| "Mess db schema with validator schema" | Standard Schema is just an interface — no validator-specific APIs leak into drizzle-orm |
| Library coupling | Works with any compliant library; no Zod/Valibot dependency in core |
| API pollution | `$validator()` stores opaque metadata, doesn't add methods to columns |
| Maintenance burden | Integration logic lives in drizzle-zod/valibot, not drizzle-orm |
---
### Checklist
- [ ] Add `$validator()` method to `ColumnBuilder`
- [ ] Add `validator` to `ColumnBuilderRuntimeConfig`
- [ ] Expose `validator` property on `Column`
- [ ] Type inference via `StandardSchemaV1.InferOutput`
- [ ] Add `@standard-schema/spec` as dev dependency (types only)
- [ ] Documentation
- [ ] Tests
---
### Related
- [Standard Schema Specification](https://github.com/standard-schema/standard-schema)
- [PR #5153: Unified createSchema API](https://github.com/drizzle-team/drizzle-orm/pull/5153)
Contributor guide
Assessment
This issue has not been assessed yet.