drizzle-team / drizzle-team/drizzle-orm

feat(drizzle-orm): Add $validator() for Standard Schema integration on columns

Open
#5,167 6 comments 9 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.