npm package: @libar-dev/zod-convex-core
Description: Core Zod v4 to Convex validator conversion engine with extensible codec registry (Zod 4.1.0+)
Author: Libar AI
Latest version: 0.1.0

# @libar-dev/zod-convex-core

[![npm version](https://img.shields.io/npm/v/@libar-dev/zod-convex-core.svg)](https://www.npmjs.com/package/@libar-dev/zod-convex-core)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.5-blue)](https://www.typescriptlang.org/)

Core Zod v4 to Convex validator conversion engine with extensible codec registry and depth
protection.

## Quick Start

Get started in 5 minutes with the most common use case: converting Zod schemas to Convex validators.

### Installation

```bash
npm install @libar-dev/zod-convex-core zod@^4
```

### Basic Usage

Convert a Zod schema to a Convex validator for mutations:

```typescript
import { zodToConvex } from '@libar-dev/zod-convex-core';
import { z } from 'zod';
import { mutation } from './_generated/server';

const CreateUserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().optional(),
}).strict();

export const createUser = mutation({
  args: zodToConvex(CreateUserSchema),
  handler: async (ctx, args) => {
    // args is fully typed: { name: string; email: string; age?: number }
    return await ctx.db.insert('users', args);
  },
});
```

That's it! Full type safety with zero boilerplate.

### Table Definitions

Use the same pattern for table schemas:

```typescript
import { defineTable } from 'convex/server';
import { zodToConvex } from '@libar-dev/zod-convex-core';

const UserTableSchema = z.object({
  name: z.string(),
  email: z.string(),
  createdAt: z.number(),
}).strict();

export const users = defineTable(zodToConvex(UserTableSchema))
  .index('by_email', ['email']);
```

### Next Steps

- **Advanced Patterns**: See [API Patterns](#api-patterns) for `zodToConvex()` vs `zodToConvexFields()`
- **Production Optimization**: Check [Build-Time vs Runtime Usage](#build-time-vs-runtime-usage) for 90% memory reduction
- **Custom Types**: Learn about [Custom Type Registration](#custom-type-registration) for Date, BigInt, and more
- **Type Safety**: Explore [Type System Features](#type-system-features) for branded types and const generics
- **Best Practices**: Read [Best Practices](#best-practices) for security and maintainability

## Why This Package?

### Community Context

At the time of writing, the official `convex-helpers` library does not yet support Zod v4:

- **Open Issue**: [convex-helpers#558](https://github.com/get-convex/convex-helpers/issues/558) -
  Zod v4 compatibility request (filed by community, no resolution yet)
- **Current State**: `convex-helpers` pins to Zod v3 via `'zod/v3'` imports with no v4 support
- **Memory Constraints**: Runtime Zod usage can hit Convex's 64MB memory limit in non-trivial
  applications (documented in our benchmarks)

This package addresses these documented gaps while maintaining compatibility with Convex's type
system.

### What This Package Provides

✅ **Zod v4 Native Support** - Uses `.brand()`, `.unwrap()`, and modern Zod v4 APIs ✅ **Extensible
Registry** - Plugin custom type converters with `registerBaseCodec()` ✅ **Depth Protection** -
Configurable limits guard against TypeScript structural depth errors ✅ **Enhanced Optional
Handling** - Proper distinction between `optional()` and `nullable()` semantics ✅ **Type-Safe ID
Support** - Integration with `@libar-dev/zod-convex-ids` for table name tracking

## When to Choose This Package

| Feature                  | convex-helpers                             | @libar-dev/zod-convex-core                                 |
| ------------------------ | ------------------------------------------ | --------------------------------------------------------- |
| **Zod Version**          | v3 only (via `'zod/v3'`)                   | Native v4 support                                         |
| **Memory Optimization**  | Runtime Zod (can hit 64MB in complex apps) | Build-time generation option (90% reduction in our tests) |
| **Custom Type Registry** | Limited                                    | Extensible via `registerBaseCodec()`                      |
| **Depth Protection**     | None                                       | Configurable limits (default: 16 levels)                  |
| **Optional Semantics**   | Basic                                      | Enhanced `.optional().nullable()` handling                |
| **Bundle Size Impact**   | ~60-65MB for typical schemas               | ~10-15MB with `@libar-dev/zod-convex-gen`                  |

**Choose convex-helpers if:**

- You're on Zod v3 and don't need to upgrade
- You have simple schemas under memory limits
- Basic Zod-to-Convex conversion is sufficient

**Choose @libar-dev/zod-convex-core if:**

- You're on Zod v4 or planning to upgrade
- You need memory optimization for production
- You have complex nested schemas requiring depth protection
- You want extensible type registry for custom conversions

## Zod 4 Support

This package is built for **Zod 4.1.0+** and leverages modern Zod 4 features for optimal performance and developer experience.

### Supported Zod 4 String Formats

All Zod 4 top-level string format functions are fully supported:

**Email & Web:**
- `z.email()` - Email address validation
- `z.url()` - URL validation

**Identifiers:**
- `z.uuid()` - Strict RFC 9562/4122 UUID validation
- `z.guid()` - Permissive GUID (8-4-4-4-12 pattern)
- `z.jwt()` - JWT token format

**Network:**
- `z.ipv4()` - IPv4 address
- `z.ipv6()` - IPv6 address
- `z.cidrv4()` - IPv4 CIDR notation
- `z.cidrv6()` - IPv6 CIDR notation

**Encoding:**
- `z.base64()` - Base64 encoding
- `z.base64url()` - URL-safe base64

**Telephony:**
- `z.e164()` - E.164 phone number format

**Date & Time (ISO 8601):**
- `z.iso.date()` - ISO date (YYYY-MM-DD)
- `z.iso.time()` - ISO time (HH:MM:SS)
- `z.iso.datetime()` - ISO datetime
- `z.iso.duration()` - ISO 8601 duration

All string formats convert to `v.string()` in Convex validators. Runtime validation is handled by Zod, while Convex validators define the storage format.

### Performance Benefits

By using Zod 4, you automatically benefit from:
- **14x faster** string parsing
- **7x faster** array parsing
- **6.5x faster** object parsing
- **100x fewer** TypeScript instantiations (faster builds, better IDE performance)
- **2.3x smaller** bundle size

### Migration from Deprecated Methods

If you're using deprecated Zod 3 string methods, migrate to top-level functions:

```typescript
// Deprecated (Zod 3 style)
z.string().email()
z.string().uuid()
z.string().url()

// Modern (Zod 4 style - tree-shakable)
z.email()
z.uuid()
z.url()
```

Both patterns work, but top-level functions are tree-shakable and recommended.

## Installation

```bash
npm install @libar-dev/zod-convex-core zod@^4.1.0
```

**Peer Dependencies:**

- `convex` >= 1.30.0 < 2.0.0
- `zod` ^4.1.0

## API Patterns

### zodToConvex() vs zodToConvexFields()

This package provides two main conversion functions. Understanding when to use each is important for
effective development.

#### zodToConvex() - The Universal Converter (Recommended Default)

**Use zodToConvex() as your default choice.** It intelligently handles both complete schemas and
plain objects.

```typescript
import { zodToConvex } from '@libar-dev/zod-convex-core';
import { z } from 'zod';

// Works with ZodObject schemas
const UserSchema = z.object({
  name: z.string(),
  age: z.number().optional(),
});

// Use in mutations/queries/actions
export const createUser = mutation({
  args: zodToConvex(UserSchema), // ✅ Recommended pattern
  handler: async (ctx, args) => { ... }
});

// Also works with plain objects (auto-delegates to zodToConvexFields)
export const updateUser = mutation({
  args: zodToConvex({
    id: zid('users'),
    name: z.string(),
  }), // ✅ Also correct - auto-converts plain object
  handler: async (ctx, args) => { ... }
});
```

#### zodToConvexFields() - The Field Converter (Special Cases)

**Use zodToConvexFields() only for spreading fields into larger definitions**, typically when
combining Zod schemas with native Convex validators.

```typescript
import { zodToConvexFields } from '@libar-dev/zod-convex-core';
import { defineTable } from 'convex/server';
import { v } from 'convex/values';

// Hybrid table pattern: Zod fields + Convex enum validators
const BaseSchema = z
  .object({
    name: z.string(),
    email: z.string().email(),
    amount: z.number(),
  })
  .strict();

export const users = defineTable({
  ...zodToConvexFields(BaseSchema.shape), // ✅ Spread Zod fields

  // Add Convex native validators for better enum type inference
  status: v.union(v.literal('active'), v.literal('inactive'), v.literal('pending')),
  role: v.union(v.literal('admin'), v.literal('user')),
})
  .index('by_email', ['email'])
  .index('by_status', ['status']);
```

### When to Use Each Pattern

| Scenario                           | Function              | Example                                                                     |
| ---------------------------------- | --------------------- | --------------------------------------------------------------------------- |
| Mutation/Query/Action args         | `zodToConvex()`       | `args: zodToConvex(Schema)`                                                 |
| Simple table definitions           | `zodToConvex()`       | `defineTable(zodToConvex(Schema))`                                          |
| Hybrid tables (Zod + Convex enums) | `zodToConvexFields()` | `defineTable({ ...zodToConvexFields(schema.shape), status: v.union(...) })` |
| Plain object validation            | `zodToConvex()`       | `zodToConvex({ field: z.string() })`                                        |
| Spreading fields only              | `zodToConvexFields()` | `{ ...zodToConvexFields(fields), extra: v.string() }`                       |

### Implementation Detail

`zodToConvex()` automatically detects input types and delegates appropriately:

```typescript
export function zodToConvex(zod, options) {
  // Detects plain objects and uses zodToConvexFields internally
  if (typeof zod === 'object' && !(zod instanceof z.ZodType)) {
    return zodToConvexFields(zod, options);
  }
  return zodToConvexInternal(zod, options);
}
```

## Schema Authoring Best Practices

### Type Alias Exports for Hybrid Pattern

When combining this package with `@libar-dev/zod-convex-gen` for build-time validator generation (the recommended approach for production), schema files **must** export type aliases.

**Why This Matters:**

The hybrid pattern uses type-only imports (`import type`) to eliminate Zod from the runtime bundle while preserving type safety. However, type-only imports are erased at compile time. Using `z.infer<typeof Schema>` with a type-only import causes TypeScript errors because `typeof` requires a runtime value.

**Schema File Pattern:**

```typescript
// src/validation-schemas/domain/myFunction.ts
import { z } from 'zod';
import { createIdValidators } from '@libar-dev/zod-convex-ids';

const ids = createIdValidators({
  user: 'users',
  company: 'companies',
} as const);

// 1. Export the schema (for @libar-dev/zod-convex-gen)
export const CreateUserArgsSchema = z.object({
  email: z.string().email(),
  name: z.string(),
  companyId: ids.companyId()
}).strict();

// 2. Export the type alias (for consumers) - REQUIRED
export type CreateUserArgs = z.infer<typeof CreateUserArgsSchema>;
```

**Consumer File Pattern:**

```typescript
// convex/domain/users.ts

// Import type alias (NOT schema) - zero runtime cost
import type { CreateUserArgs } from '../../src/validation-schemas/domain/users';

// Import generated validators (from @libar-dev/zod-convex-gen)
import { createUserArgsFields } from '../generatedValidators/users';

export const createUser = mutation({
  args: createUserArgsFields,                 // Runtime: Pure Convex validators
  handler: async (ctx, args: CreateUserArgs) => { // Types: Type alias from Zod
    // Full type safety, zero Zod in bundle (90% memory reduction)
    await ctx.db.insert('users', {
      email: args.email,
      name: args.name,
      companyId: args.companyId
    });
  }
});
```

**Naming Convention:**

- Function arguments: Remove "Schema" suffix (`CreateUserArgsSchema` → `CreateUserArgs`)
- Table schemas: Use singular form (`UsersTableSchema` → `User`)

**Common Mistakes:**

```typescript
// ❌ Missing type alias export
export const MyArgsSchema = z.object({...}).strict();
// Missing: export type MyArgs = z.infer<typeof MyArgsSchema>;

// ❌ Using typeof with type-only import (breaks compilation)
import type { MyArgsSchema } from '...';
handler: async (ctx, args: z.infer<typeof MyArgsSchema>) => { ... } // ERROR!

// ✅ Correct: Import and use type alias
import type { MyArgs } from '...';
handler: async (ctx, args: MyArgs) => { ... }
```

**When to Use This Pattern:**

- **Production applications**: Use `@libar-dev/zod-convex-gen` with type aliases (90% memory reduction)
- **Development/prototyping**: Use runtime `zodToConvex()` directly (simpler, no build step)

**See Also:**

- `@libar-dev/zod-convex-gen` - Build-time validator generation package
- [Getting Started Guide](../../docs/GETTING_STARTED.md)

This means you can safely use `zodToConvex()` for most scenarios, and it will handle the conversion
correctly.

## Basic Usage

### Converting Zod Schemas

```typescript
import { zodToConvex } from '@libar-dev/zod-convex-core';
import { z } from 'zod';
import { mutation } from './_generated/server';

const CreatePostSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string(),
  published: z.boolean().default(false),
  tags: z.array(z.string()).optional(),
});

export const createPost = mutation({
  args: zodToConvex(CreatePostSchema),
  handler: async (ctx, args) => {
    // args is fully typed and validated
    return await ctx.db.insert('posts', args);
  },
});
```

### Table Definitions

```typescript
import { defineTable } from 'convex/server';
import { zodToConvex } from '@libar-dev/zod-convex-core';

const PostSchema = z.object({
  title: z.string(),
  content: z.string(),
  authorId: zid('users'),
  published: z.boolean(),
  createdAt: z.number(),
});

// Simple table (all Zod)
export const posts = defineTable(zodToConvex(PostSchema))
  .index('by_author', ['authorId'])
  .index('by_published', ['published', 'createdAt']);
```

## Build-Time vs Runtime Usage

This package provides **runtime conversion** of Zod schemas to Convex validators. For production
applications hitting Convex's 64MB memory limit, consider build-time generation instead.

### When to Use Runtime Conversion (This Package)

**Use `@libar-dev/zod-convex-core` when:**

- ✅ Development and prototyping
- ✅ Applications under 64MB memory limit
- ✅ Need dynamic schema composition
- ✅ Testing and experimentation

**Memory Profile:**

- Table schemas with Zod: ~27MB
- Function args with Zod: ~18MB
- Complex unions with Zod: ~10MB
- **Total**: ~55-60MB of 64MB limit

### When to Use Build-Time Generation

**Use `@libar-dev/zod-convex-gen` when:**

- ✅ Production deployment
- ✅ Hitting 64MB memory limit
- ✅ Want 90% memory reduction
- ✅ Schema definitions are stable
- ✅ Need maximum performance

**Memory Profile:**

- Table schemas (generated): ~3MB
- Function args (hybrid pattern): ~10-15MB total
- **Total**: ~10-15MB of 64MB limit

**Memory Comparison:**

| Approach                       | Bundle Size | Memory Usage | Zod in Bundle | Best For    |
| ------------------------------ | ----------- | ------------ | ------------- | ----------- |
| **Runtime Zod** (this package) | 60-65MB     | High         | ✅ Yes        | Development |
| **Build-Time Generation**      | 10-15MB     | Low          | ❌ No         | Production  |

### Use Case Decision Matrix

| Scenario                   | Recommended Approach  | Reasoning                                                |
| -------------------------- | --------------------- | -------------------------------------------------------- |
| Early development          | Runtime Zod           | Fast iteration, no build step                            |
| Dynamic schema composition | Runtime Zod           | Schema generation at runtime                             |
| Production app < 40MB      | Runtime Zod           | No memory pressure                                       |
| Production app > 50MB      | Build-time generation | Avoid memory limit                                       |
| Large schema collections   | Build-time generation | Significant savings                                      |
| Hybrid approach needed     | Both                  | Use generated for tables, runtime for dynamic validation |

### Important Limitations & Fallback Behavior

**⚠️ Zod Validation Features Not Preserved**

The following Zod features **lose their validation logic** during conversion and fall back to
`v.any()`:

**`.refine()` - Custom Validation Logic**

```typescript
// ❌ Validation logic is LOST
const EmailSchema = z.string().refine(s => s.includes('@'), {
  message: 'Must be valid email',
});
zodToConvex(EmailSchema); // Returns v.any() ⚠️

// ✅ SOLUTION: Apply refinements at function boundaries, not table schemas
export const createUser = mutation({
  args: zodToConvex(
    z.object({
      email: z.string(), // Structure only in table
    })
  ),
  handler: async (ctx, args) => {
    // Validate business rules here
    const validated = EmailSchema.parse(args.email);
  },
});
```

**`.transform()` - Type Transformations**

```typescript
// ❌ Transform logic is LOST (unless registered codec exists)
const UpperSchema = z.string().transform(s => s.toUpperCase());
zodToConvex(UpperSchema); // Returns v.any() ⚠️

// ✅ EXCEPTION: Date has built-in codec support
const DateSchema = z.date(); // Automatically uses Date codec
zodToConvex(DateSchema); // Returns v.float64() with Date ↔ number conversion ✅

// ✅ SOLUTION: Register custom codec for your transform
import { registerBaseCodec } from '@libar-dev/zod-convex-core';

registerBaseCodec({
  check: schema => schema instanceof z.ZodEffects && schema._def.effect.type === 'transform',
  toValidator: () => v.string(),
  fromConvex: value => value,
  toConvex: value => (typeof value === 'string' ? value.toUpperCase() : value),
});
```

**`.pipe()` - Schema Chaining**

```typescript
// ❌ Falls back to v.any() unless registered codec
const PipedSchema = z.string().pipe(z.string().min(5));
zodToConvex(PipedSchema); // Returns v.any() ⚠️
```

**Why This Happens:**

- **Convex validators** validate structure only (types, presence, constraints)
- **Zod refinements** contain arbitrary JavaScript logic that can't be serialized to Convex
- **Only exception**: Registered codecs (like Date) provide bidirectional conversion rules

**Best Practices:**

1. **Table schemas** → Use structural validation only (types, optionality, unions)
2. **Function arguments** → Apply `.refine()` and business logic validation
3. **Custom types** → Register codecs via `@libar-dev/zod-convex-codecs` package
4. **Date handling** → Built-in codec automatically converts Date ↔ timestamp

**See Also:**

- [@libar-dev/zod-convex-codecs](https://www.npmjs.com/package/@libar-dev/zod-convex-codecs) - Custom
  type codec library
- [Codec Registry Documentation](#codec-registration) - How to register custom codecs

### Migration Path

When you're ready to optimize for production:

**Step 1: Install Generator**

```bash
npm install --save-dev @libar-dev/zod-convex-gen
```

**Step 2: Generate Validators**

```bash
npx zod-convex-gen
```

**Step 3: Update Imports**

```typescript
// Before (Runtime - uses this package)
import { zodToConvex } from '@libar-dev/zod-convex-core';
import { UsersTableSchema } from './schemas/users';

export default defineSchema({
  users: defineTable(zodToConvex(UsersTableSchema)),
});

// After (Build-time - eliminates Zod from bundle)
import { usersFields, usersEnums } from './generatedValidators/users';

export default defineSchema({
  users: defineTable({
    ...usersFields,
    ...usersEnums,
  }),
});
```

**Result**: 90% memory reduction while maintaining full type safety

### Related Packages

- **[@libar-dev/zod-convex-gen](https://www.npmjs.com/package/@libar-dev/zod-convex-gen)** -
  Build-time validator generation (90% memory reduction)
- **[@libar-dev/zod-convex-builders](https://www.npmjs.com/package/@libar-dev/zod-convex-builders)** -
  Function wrappers with Zod validation (supports both runtime and generated validators)

**Key Insight**: This package (`@libar-dev/zod-convex-core`) is the foundation that powers both
runtime conversion AND build-time generation. The generator uses this package's conversion logic at
build-time to eliminate Zod from the runtime bundle.

## Type Safety

Both functions preserve full TypeScript type inference:

```typescript
// Type is fully inferred
const validator = zodToConvex(UserSchema);
// validator: ConvexValidator<{ name: string; age?: number }>

// Field types are preserved
const fields = zodToConvexFields(UserSchema.shape);
// fields: { name: VString; age: VOptional<VNumber> }
```

## Advanced Features

### Depth Limit Protection

The converter guards against excessive recursion depth that could cause:

- Slow TypeScript type checking
- Memory exhaustion during codegen
- Exceeding Convex's 64MB initialization limit
- TypeScript structural depth errors

**Default Behavior:**

- **Maximum depth**: 16 levels (configurable)
- **Warning threshold**: 80% of max (level 13 by default)
- **Fallback behavior**: Short-circuits to `v.any()` when limit exceeded

**Configuration:**

```typescript
import { zodToConvex } from '@libar-dev/zod-convex-core';

// Use custom depth limit
const validator = zodToConvex(schema, {
  maxDepth: 12, // Adjust based on schema complexity
  onWarning: warning => {
    if (warning.type === 'depth_limit_warning') {
      console.warn(`Schema depth approaching limit at ${warning.depth} levels:`, warning.message);
    }
    if (warning.type === 'depth_limit_exceeded') {
      console.error(
        `Schema depth exceeded at ${warning.depth} levels - using v.any()`,
        warning.message
      );
    }
  },
});
```

**When to Adjust:**

- **Increase maxDepth** if you have legitimately deep schemas (e.g., nested configuration objects)
- **Decrease maxDepth** if experiencing slow type checking or memory issues
- **Monitor warnings** to identify problematic schemas before they cause issues

**Recommendations:**

- Keep schemas under 10 levels deep when possible
- Use composition and unions instead of deep nesting
- Consider flattening deeply nested structures
- Default 16 works for most real-world use cases (99%+ of schemas)

### Type System Features

This package provides cutting-edge TypeScript patterns for enhanced type safety and developer experience. All features are **100% backward compatible** with opt-in enhancements.

#### Const Type Parameters (Automatic)

Const generics preserve literal types through the conversion pipeline, providing better autocomplete and stricter type checking **without any code changes**.

```typescript
import { zodToConvex } from '@libar-dev/zod-convex-core';

// Literal types are automatically preserved
const RoleSchema = z.object({
  role: z.literal('admin'),
  permissions: z.enum(['read', 'write', 'delete'] as const),
});

const validator = zodToConvex(RoleSchema);
// role is preserved as 'admin', permissions as 'read' | 'write' | 'delete' ✨

// Better autocomplete for discriminated unions
const EventSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('click'), x: z.number() }),
  z.object({ type: z.literal('keypress'), key: z.string() }),
]);

// TypeScript now narrows correctly based on 'type' field
```

**Learn More:** [ADR 001: Const Type Parameters](./docs/adr/001-const-type-parameters.md)

#### Branded Types (Opt-In)

Branded types prevent accidental mixing of semantically different strings at compile time with **zero runtime cost**.

##### Package-Specific Branded Types

```typescript
import {
  type CodecId,
  type ValidatorPath,
  asCodecId,
  asValidatorPath,
} from '@libar-dev/zod-convex-core';

// Type-safe codec IDs
const codecId: CodecId = asCodecId('date-codec');
const path: ValidatorPath = asValidatorPath('user.email');

// Can't accidentally mix types
processCodec(path); // ✅ Type error! ValidatorPath not assignable to CodecId
processCodec(codecId); // ✅ Works
```

##### Generic Branded Type Utility

Create custom branded types for your own use cases:

```typescript
import { brand, unbrand } from '@libar-dev/zod-convex-core';

// Define custom branded types
type UserId = string & { __brand: 'UserId' };
type SessionId = string & { __brand: 'SessionId' };
type Timestamp = number & { __brand: 'Timestamp' };

// Create branded values
const userId = brand<string, 'UserId'>('user_12345');
const sessionId = brand<string, 'SessionId'>('session_abc');
const timestamp = brand<number, 'Timestamp'>(Date.now());

// Type safety prevents mixing
const wrong: UserId = sessionId; // ✅ Type error!

// Unwrap when needed (logging, API boundaries)
console.log(`User: ${unbrand(userId)}`);

// Recommended pattern: Create helper functions
const asUserId = (id: string): UserId => brand<string, 'UserId'>(id);
const asSessionId = (id: string): SessionId => brand<string, 'SessionId'>(id);
```

**Benefits:**
- Compile-time safety (catches bugs before runtime)
- Self-documenting code (clear type semantics)
- Better IDE support (autocomplete, type narrowing)
- Zero runtime overhead (brands erased at compile time)

**Learn More:** [ADR 002: Branded Types](./docs/adr/002-branded-types.md)

#### Strict Mode Helpers (Opt-In)

Security-first schema creation that rejects unknown fields by default, preventing data pollution from external sources.

```typescript
import { createStrictSchema, ensureStrict } from '@libar-dev/zod-convex-core';

// ✅ Recommended: Use createStrictSchema() as default
const UserSchema = createStrictSchema({
  name: z.string(),
  email: z.string().email(),
});

// Rejects unknown fields (security benefit)
UserSchema.parse({
  name: 'Alice',
  email: 'alice@test.com',
  malicious: '<script>xss</script>', // ✅ Rejected!
});
// Throws: Unrecognized key(s) in object: 'malicious'

// For existing schemas
const ExistingSchema = z.object({...});
const StrictSchema = ensureStrict(ExistingSchema);

// Aligns with security best practices:
// - Prevents data pollution from AI providers
// - Blocks malicious input (XSS, injection)
// - Enforces explicit field acceptance
```

**Learn More:** [ADR 003: Validation Schema Pattern](./docs/adr/003-validation-schema-pattern.md)

### Enhanced Codec Management

This package provides full lifecycle management for custom codecs with branded IDs.

```typescript
import {
  registerBaseCodec,
  unregisterCodec,
  getCodecById,
  getAllCodecIds,
  type BaseCodec,
  type CodecId,
} from '@libar-dev/zod-convex-core';

// Register codec with explicit ID
const codecId = registerBaseCodec({
  check: schema => schema instanceof z.ZodBigInt,
  toValidator: () => v.int64(),
  fromConvex: value => BigInt(value),
  toConvex: value => Number(value),
}, 'bigint-codec'); // Returns: CodecId

// Later: Unregister codec (useful for testing)
const removed = unregisterCodec(codecId);
console.log(removed); // true

// Inspect registered codecs
const codec = getCodecById(codecId); // Get codec by ID
const allIds = getAllCodecIds(); // List all registered codecs

// Example: Test cleanup
describe('Codec Tests', () => {
  let testCodecId: CodecId;

  beforeEach(() => {
    testCodecId = registerBaseCodec(testCodec, 'test-codec');
  });

  afterEach(() => {
    unregisterCodec(testCodecId); // Clean up
  });
});
```

**Features:**
- `registerBaseCodec()` returns a `CodecId` for lifecycle management
- `unregisterCodec(id)` - Remove codec by ID
- `getCodecById(id)` - Lookup codec
- `getAllCodecIds()` - List all registered codecs
- Codecs are checked in reverse registration order (last registered = highest priority)

### Custom Type Registration

```typescript
import { v } from 'convex/values';
import { z } from 'zod';
import { registerBaseCodec } from '@libar-dev/zod-convex-core';

// Register custom type conversions (example: BigInt support)
registerBaseCodec({
  check: schema => schema instanceof z.ZodBigInt,
  toValidator: () => v.int64(),
  fromConvex: value => BigInt(value),
  toConvex: value => Number(value),
});
```

### ID Validation

```typescript
import { zid } from '@libar-dev/zod-convex-ids';

const schema = z.object({
  userId: zid('users'), // Type-safe Convex ID
  postId: zid('posts'), // Validates table name at type level
});
```

### Complex Type Handling

The converter handles advanced Zod patterns with full type safety:

#### Optional and Nullable Types

```typescript
import { zodToConvex } from '@libar-dev/zod-convex-core';
import { z } from 'zod';

const ProfileSchema = z
  .object({
    bio: z.string().optional(), // Optional field
    avatar: z.string().nullable(), // Nullable field
    banner: z.string().optional().nullable(), // Both optional and nullable
  })
  .strict();

// Converted to Convex validators:
// bio: v.optional(v.string())
// avatar: v.union(v.string(), v.null())
// banner: v.optional(v.union(v.string(), v.null()))
```

**Conversion Rules:**

- `.optional()` → `v.optional(...)`
- `.nullable()` → `v.union(..., v.null())`
- `.optional().nullable()` → `v.optional(v.union(..., v.null()))`

**Note on Defaults**: Zod defaults (`.default()`) are not preserved in Convex validators, as Convex
doesn't support default values. Handle defaults in your application logic.

#### Complex Optional Chains

The converter preserves optional semantics through nested structures:

```typescript
const ComplexSchema = z
  .object({
    // Nested optional object
    preferences: z
      .object({
        theme: z.string(),
        notifications: z.boolean(),
      })
      .optional(),

    // Optional array of objects
    tags: z
      .array(
        z.object({
          label: z.string(),
          color: z.string().optional(),
        })
      )
      .optional(),

    // Nullable with nested structure
    metadata: z
      .object({
        source: z.string(),
        timestamp: z.number(),
      })
      .nullable(),
  })
  .strict();

// All optional/nullable semantics are preserved in conversion
const validator = zodToConvex(ComplexSchema);
```

#### Record Types

`z.record()` **is supported** and converts to `v.record()`, but with important limitations:

```typescript
// ✅ SUPPORTED: Record with string keys
const MetadataSchema = z.record(z.string(), z.number());
// Converts to: v.record(v.string(), v.float64())

// ✅ SUPPORTED: Records with optional values
const ConfigSchema = z.record(z.string(), z.string().optional());
// Converts to: v.record(v.string(), v.union(v.string(), v.null()))

// ⚠️ LIMITATION: Only string keys supported (Convex constraint)
const EnumKeyRecord = z.record(z.enum(['admin', 'user']), z.string());
// Falls back to: v.record(v.string(), v.any()) ⚠️

// ✅ RECOMMENDED: Prefer specific object schemas when keys are known
const StaticConfig = z
  .object({
    apiKey: z.string(),
    endpoint: z.string(),
    timeout: z.number(),
  })
  .strict();
```

**Key Limitations:**

- **String keys only** - Convex `v.record()` only accepts `v.string()` keys (no enums, numbers,
  etc.)
- **Type safety** - Static object schemas provide better TypeScript inference than dynamic records
- **Performance** - Specific object shapes are more efficient than dynamic records

**When to Use Records:**

- ✅ Truly dynamic keys unknown at compile time
- ✅ User-provided metadata or configuration
- ✅ Temporary data structures during processing

**When to Use Objects:**

- ✅ Keys known at schema definition time (vast majority of cases)
- ✅ Better type inference needed
- ✅ Optimal performance required

#### Nested Object Structures

The converter supports deep nesting with depth limit protection (default: 16 levels):

```typescript
const DeepSchema = z
  .object({
    level1: z.object({
      level2: z.object({
        level3: z.object({
          value: z.string(),
        }),
      }),
    }),
  })
  .strict();

// Fully supported - converted to nested v.object() validators
const validator = zodToConvex(DeepSchema);
```

**Best Practice**: Keep schemas under 10 levels deep when possible. Use composition and unions
instead of excessive nesting.

## Best Practices

### Core Practices

1. **Default to `zodToConvex()`** - It handles most use cases automatically
2. **Use `.strict()` on schemas** - Ensures no unexpected fields: `z.object({...}).strict()`
3. **Leverage type inference** - Let TypeScript infer types rather than declaring manually
4. **Hybrid tables for enums** - Use native Convex validators for better enum type inference
5. **Single source of truth** - Define schemas once in Zod, derive everything else

### Enhanced Practices

6. **Use `createStrictSchema()` as default** - Security-first schema creation
   ```typescript
   // ✅ Recommended
   const UserSchema = createStrictSchema({ name: z.string() });

   // Instead of:
   const UserSchema = z.object({ name: z.string() }).strict();
   ```

7. **Leverage const generics for literals** - Automatic literal type preservation
   ```typescript
   const schema = z.object({
     role: z.literal('admin'),  // Preserved as 'admin' (not string)
     status: z.enum(['active', 'inactive'] as const),  // Preserved as union
   });
   ```

8. **Register codecs before conversion** - For optimal performance
   ```typescript
   const codecId = registerBaseCodec(myCodec, 'my-codec');
   // Then convert schemas...
   ```

9. **Use branded types for type-safe IDs** - Prevent string mixing bugs
   ```typescript
   const codecId: CodecId = asCodecId('date-codec');
   // Can't accidentally pass wrong string type
   ```

10. **Separate structural from business validation** - Security and maintainability
    ```typescript
    const Structural = createStrictSchema({ email: z.string() });
    const BusinessRules = Structural.extend({}).refine(...);
    ```

**See Also:**
- [Type System Features](#type-system-features) - Package capabilities
- [Validation Schema Pattern (ADR 003)](./docs/adr/003-validation-schema-pattern.md) - Structural vs business rules

## Supported Type Mappings

Complete mapping of Zod types to Convex validators:

| Zod Type           | Convex Validator          | Notes                                                                     |
| ------------------ | ------------------------- | ------------------------------------------------------------------------- |
| `z.string()`       | `v.string()`              | Direct mapping                                                            |
| `z.number()`       | `v.float64()`             | All numbers as float64                                                    |
| `z.bigint()`       | `v.int64()`               | BigInt support                                                            |
| `z.boolean()`      | `v.boolean()`             | Direct mapping                                                            |
| `z.date()`         | `v.float64()`             | Stored as Unix timestamp (use @libar-dev/zod-convex-codecs for conversion) |
| `z.null()`         | `v.null()`                | Direct mapping                                                            |
| `z.undefined()`    | Not supported             | Omit field instead                                                        |
| `z.array(T)`       | `v.array(T)`              | Recursive conversion                                                      |
| `z.object({...})`  | `v.object({...})`         | Field-by-field conversion                                                 |
| `z.union([...])`   | `v.union(...)`            | All branches converted                                                    |
| `z.enum([...])`    | `v.union(...)`            | Literals for each value                                                   |
| `z.literal(value)` | `v.literal(value)`        | Direct mapping                                                            |
| `z.optional(T)`    | `v.optional(T)`           | Convex optional wrapper                                                   |
| `z.nullable(T)`    | `v.union(T, v.null())`    | Union with null                                                           |
| `z.record(K, V)`   | `v.record(v.string(), V)` | ✅ Supported (string keys only, prefer object schemas for known keys)     |
| `zid(table)`       | `v.id(table)`             | Via @libar-dev/zod-convex-ids                                              |

## Known Limitations & Solutions

### Enum Type Inference with TypeScript

**Issue**: When using `zodToConvexFields()` with `z.enum()` fields, TypeScript may incorrectly infer
the field types as `undefined` in the resulting DataModel, causing `GenericDataModel` constraint
errors.

**Important**: This is a TypeScript type inference limitation, NOT a runtime bug. The validators
work correctly at runtime.

**Solution**: Use the Hybrid Approach for tables containing enum fields:

```typescript
// ❌ Problem: Pure Zod approach fails TypeScript inference for enums
const Schema = z
  .object({
    userId: zid('users'),
    role: z.enum(['admin', 'member', 'viewer']), // TypeScript infers as undefined
    status: z.enum(['active', 'pending']), // TypeScript infers as undefined
  })
  .strict();

export const table = defineTable(
  zodToConvexFields(Schema.shape) // Causes GenericDataModel errors!
);

// ✅ Solution: Hybrid approach - Zod for regular fields, Convex for enums
const BaseSchema = z
  .object({
    userId: zid('users'),
    companyId: zid('companies'),
  })
  .strict();

export const table = defineTable({
  ...zodToConvexFields(BaseSchema.shape), // Zod for regular fields
  role: v.union(
    // Convex for enum fields
    v.literal('admin'),
    v.literal('member'),
    v.literal('viewer')
  ),
  status: v.union(v.literal('active'), v.literal('pending')),
}).index('by_user', ['userId']);
```

### Complex Unions with GenericDataModel

**Challenge**: Complex discriminated unions (20+ branches with nested `zid()`) may exceed
TypeScript's structural depth limits:

```typescript
// ✅ Conversion SUCCEEDS
const EventPayloadSchema = z.union([
  z.object({ type: z.literal('user.created'), userId: zid('users') }),
  z.object({ type: z.literal('post.created'), postId: zid('posts') }),
  // ... 20+ more schemas
]);

const validator = zodToConvex(EventPayloadSchema); // Works!

// ❌ But using in schema causes TypeScript errors
export default defineSchema({
  events: defineTable({
    payload: zodToConvex(EventPayloadSchema), // TypeScript error!
  }),
});
// Error: Type 'DataModel' not assignable to 'GenericDataModel'
```

**Solution**: Use validation-at-boundary pattern with `v.any()`:

```typescript
// 1. Schema: Use v.any() as storage contract
export default defineSchema({
  events: defineTable({
    // @architectural-directive: validation-at-boundary
    // Complex union exceeds TypeScript limits, validated at write boundaries
    payload: v.any(),
  }),
});

// 2. Validation: Enforce at ALL write boundaries
export const createEvent = mutation({
  args: zodToConvex(
    z.object({
      payload: EventPayloadSchema, // ← Full validation here
    })
  ),
  handler: async (ctx, args) => {
    await ctx.db.insert('events', args);
  },
});
```

## Migration from convex-helpers

Step-by-step guide to migrate from `convex-helpers/server/zod`:

### Step 1: Update Dependencies

```diff
{
  "dependencies": {
+   "@libar-dev/zod-convex-core": "^0.1.0",
+   "zod": "^4.1.0"
  },
  "devDependencies": {
-   "convex-helpers": "^0.1.x"  // Can keep for other features
  }
}
```

```bash
# Install new packages
npm install @libar-dev/zod-convex-core zod@^4.1.0

# Optional: Install related packages for enhanced features
npm install @libar-dev/zod-convex-ids    # Type-safe ID validation
npm install @libar-dev/zod-convex-codecs  # Date and custom type codecs
```

### Step 2: Update Imports

**Before (convex-helpers):**

```typescript
import { zodToConvex, zid } from 'convex-helpers/server/zod';
import { z } from 'zod/v3'; // Zod v3 import
```

**After (@libar-dev/zod-convex-core):**

```typescript
import { zodToConvex } from '@libar-dev/zod-convex-core';
import { zid } from '@libar-dev/zod-convex-ids'; // Separate package
import { z } from 'zod'; // Zod v4 (no /v3 suffix)
```

### Step 3: Verify Build Order

**IMPORTANT**: Follow this build sequence to avoid type errors:

```bash
# 1. Build packages (if in monorepo)
npm run build:packages

# 2. Generate Convex types
npx convex codegen

# 3. Validate TypeScript
npm run check:fast
# or: npx tsc --noEmit
```

### Step 4: Leverage Zod v4 Features

```typescript
// Zod v4: Native .brand() support
const UserIdSchema = z.string().brand('UserId');

// Zod v4: .unwrap() on optional/nullable
const schema = z.string().optional();
const inner = schema.unwrap(); // Returns z.string()

// Zod v4: Improved discriminated unions
const EventSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
  z.object({ type: z.literal('keypress'), key: z.string() }),
]);
```

### Step 5: Handle Enum Fields

If you encounter TypeScript errors with enum fields:

```typescript
// Use the hybrid approach documented above
const BaseSchema = z.object({
  // non-enum fields
});

export const table = defineTable({
  ...zodToConvexFields(BaseSchema.shape),
  enumField: v.union(v.literal('opt1'), v.literal('opt2')),
});
```

## Comparison with convex-helpers

| Feature                   | convex-helpers        | @libar-dev/zod-convex-core      |
| ------------------------- | --------------------- | ------------------------------ |
| **Zod Version**           | v3 only (`zod/v3`)    | v4 native                      |
| **ID Validation**         | Basic `zid()`         | Separate package with metadata |
| **Depth Protection**      | None                  | Configurable limits            |
| **Custom Type Registry**  | Limited               | `registerBaseCodec()`          |
| **Optional Handling**     | Basic                 | Enhanced semantics             |
| **Brand Support**         | Custom implementation | Native Zod v4 `.brand()`       |
| **Enum Type Inference**   | Works                 | Hybrid approach needed         |
| **Complex Union Support** | Limited               | Validation-at-boundary pattern |

## Related Packages

- [`@libar-dev/zod-convex-ids`](https://www.npmjs.com/package/@libar-dev/zod-convex-ids) - Type-safe
  ID validation
- [`@libar-dev/zod-convex-codecs`](https://www.npmjs.com/package/@libar-dev/zod-convex-codecs) - Date
  and custom type codecs
- [`@libar-dev/zod-convex-builders`](https://www.npmjs.com/package/@libar-dev/zod-convex-builders) -
  Function builders
- [`@libar-dev/zod-convex-tables`](https://www.npmjs.com/package/@libar-dev/zod-convex-tables) - Table
  utilities

## License

MIT License - See [LICENSE](./LICENSE) file for details.

Copyright (c) 2025 Libar AI