@libar-dev/zod-convex-codecs
v0.1.0
Published
Bidirectional codecs for Date and custom type conversion with Convex
Downloads
48
Maintainers
Readme
@libar-dev/zod-convex-codecs
Bidirectional codecs for Date and custom type conversion between Zod schemas and Convex storage format.
Why This Package?
This package provides automatic encoding/decoding for complex types when working with Convex:
- Automatic Date Conversion - Seamlessly convert between JavaScript
Dateand Unix timestamps - Bidirectional Codecs - Encode for storage, decode for retrieval
- Custom Type Support - Register your own type converters via
@libar-dev/zod-convex-core - Schema-Aware - Respects your Zod schema structure
- Zero Configuration - Date handling works out of the box
Installation
npm install @libar-dev/zod-convex-codecs zod@^4.1.0Peer Dependencies:
convex>= 1.30.0 < 2.0.0zod^4.1.0
Dependencies:
@libar-dev/zod-convex-core- Core conversion engine (includes ID validation support)
Quick Start
Automatic Date Conversion
import { convexCodec } from '@libar-dev/zod-convex-codecs';
import { z } from 'zod';
// Define schema with Date types
const EventSchema = z.object({
title: z.string(),
eventDate: z.date(),
createdAt: z.date(),
});
// Create bidirectional codec
const codec = convexCodec(EventSchema);
// Encode: JavaScript Date → Unix timestamp (for Convex storage)
const convexData = codec.encode({
title: 'Team Meeting',
eventDate: new Date('2024-01-15T10:00:00Z'),
createdAt: new Date(),
});
// Result: { title: 'Team Meeting', eventDate: 1705316400000, createdAt: 1705316400000 }
// Decode: Unix timestamp → JavaScript Date (for application use)
const jsData = codec.decode(convexData);
// Result: { title: 'Team Meeting', eventDate: Date(2024-01-15), createdAt: Date }Use in Convex Functions
import { convexCodec } from '@libar-dev/zod-convex-codecs';
import { mutation, query } from './_generated/server';
import { v } from 'convex/values';
const EventSchema = z.object({
title: z.string(),
startDate: z.date(),
endDate: z.date(),
});
const eventCodec = convexCodec(EventSchema);
// Mutation: Dates automatically encoded to timestamps
export const createEvent = mutation({
args: eventCodec.validator,
handler: async (ctx, args) => {
// args has Dates as timestamps (Convex-safe)
const eventId = await ctx.db.insert('events', args);
return eventId;
},
});
// Query: Decode timestamps back to Dates for client
export const getEvent = query({
args: { id: v.id('events') },
handler: async (ctx, args) => {
const event = await ctx.db.get(args.id);
if (!event) return null;
// Decode timestamps back to Date objects
return eventCodec.decode(event);
},
});
// Client receives proper Date objects
const event = await convex.query(api.events.getEvent, { id: eventId });
console.log(event.startDate instanceof Date); // trueAPI Documentation
convexCodec(schema)
Creates a bidirectional codec for encoding/decoding between JavaScript and Convex types.
import { convexCodec } from '@libar-dev/zod-convex-codecs';
const UserSchema = z.object({
name: z.string(),
birthDate: z.date(),
lastLogin: z.date().nullable(),
metadata: z.object({
createdAt: z.date(),
updatedAt: z.date(),
}),
});
const codec = convexCodec(UserSchema);
// Codec provides:
codec.validator; // Convex validator for the schema
codec.encode(); // Convert JS types to Convex types
codec.decode(); // Convert Convex types to JS types
codec.pick(); // Create codec for subset of fieldsKey Features:
- Automatic Date ↔ timestamp conversion
- Handles nested objects and arrays
- Preserves nullable and optional semantics
- Type-safe encode/decode operations
toConvexJS(schema, value) / toConvexJS(value)
Converts JavaScript values to Convex-safe JSON format.
import { toConvexJS } from '@libar-dev/zod-convex-codecs';
// With schema (schema-aware conversion)
const UserSchema = z.object({
name: z.string(),
createdAt: z.date(),
});
const convexValue = toConvexJS(UserSchema, {
name: 'Alice',
createdAt: new Date('2024-01-15'),
});
// Result: { name: 'Alice', createdAt: 1705276800000 }
// Without schema (basic conversion)
const basicValue = toConvexJS({
count: 42,
timestamp: new Date(),
items: ['a', 'b', 'c'],
optional: undefined, // Will be removed
});
// Dates → timestamps, undefined → removedConversion Rules:
Dateobjects → Unix timestamps (milliseconds)undefinedvalues → removed from objects- Nested objects/arrays → recursively converted
fromConvexJS(value, schema)
Converts Convex JSON back to JavaScript types.
import { fromConvexJS } from '@libar-dev/zod-convex-codecs';
const EventSchema = z.object({
title: z.string(),
eventDate: z.date(),
});
const convexData = {
title: 'Meeting',
eventDate: 1705276800000, // Unix timestamp from Convex
};
const jsData = fromConvexJS(convexData, EventSchema);
// Result: { title: 'Meeting', eventDate: new Date(2024-01-15) }Pick Subset of Fields
Create a codec for just specific fields:
const FullSchema = z.object({
id: z.string(),
name: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
deletedAt: z.date().nullable(),
});
const fullCodec = convexCodec(FullSchema);
// Create codec for just date fields
const dateCodec = fullCodec.pick(['createdAt', 'updatedAt', 'deletedAt']);
const dates = dateCodec.encode({
createdAt: new Date('2024-01-15'),
updatedAt: new Date('2024-01-16'),
deletedAt: null,
});
// { createdAt: 1705276800000, updatedAt: 1705363200000, deletedAt: null }Advanced Usage
Complex Nested Schemas
The codec system handles deeply nested structures:
const OrderSchema = z.object({
orderId: z.string(),
items: z.array(
z.object({
productId: zid('products'),
quantity: z.number(),
addedAt: z.date(),
})
),
totalAmount: z.number(),
placedAt: z.date(),
shippedAt: z.date().nullable(),
metadata: z.object({
createdBy: z.string(),
timestamps: z.object({
created: z.date(),
modified: z.date(),
}),
}),
});
const orderCodec = convexCodec(OrderSchema);
// All Date fields at any nesting level are handled
const encoded = orderCodec.encode({
orderId: 'ORD-123',
items: [
{
productId: 'prod_abc123' as Id<'products'>,
quantity: 2,
addedAt: new Date(),
},
],
totalAmount: 49.99,
placedAt: new Date(),
shippedAt: null,
metadata: {
createdBy: 'user_123',
timestamps: {
created: new Date(),
modified: new Date(),
},
},
});
// All Date objects converted to timestampsCustom Type Registration
Register custom type converters for specialized Zod types:
import { registerBaseCodec } from '@libar-dev/zod-convex-core';
import { v } from 'convex/values';
// Example: BigInt support
registerBaseCodec({
check: schema => schema instanceof z.ZodBigInt,
toValidator: () => v.int64(),
fromConvex: value => BigInt(value),
toConvex: value => Number(value),
});
// Now BigInt fields work automatically with codecs
const schema = z.object({
balance: z.bigint(),
});
const codec = convexCodec(schema);Codec-Based API Layer Pattern
Build a type-safe API layer with automatic conversion:
// shared/schemas.ts
export const EventSchema = z.object({
id: z.string(),
title: z.string(),
startDate: z.date(),
endDate: z.date(),
attendees: z.array(zid('users')),
});
// convex/events.ts
import { convexCodec } from '@libar-dev/zod-convex-codecs';
import { EventSchema } from '../shared/schemas';
const eventCodec = convexCodec(EventSchema);
// All mutations use the same codec
export const createEvent = mutation({
args: eventCodec.validator,
handler: async (ctx, args) => {
const id = await ctx.db.insert('events', args);
return id;
},
});
export const updateEvent = mutation({
args: {
id: v.id('events'),
data: eventCodec.validator,
},
handler: async (ctx, { id, data }) => {
await ctx.db.patch(id, data);
},
});
// Queries decode on return
export const listEvents = query({
handler: async ctx => {
const events = await ctx.db.query('events').collect();
return events.map(event => eventCodec.decode(event));
},
});Working with Dates
Date Handling Details
Dates are automatically converted between JavaScript Date objects and Unix timestamps:
// Encoding (JS → Convex)
const encoded = codec.encode({ eventDate: new Date('2024-01-15T10:00:00Z') });
// { eventDate: 1705316400000 }
// Decoding (Convex → JS)
const decoded = codec.decode({ eventDate: 1705316400000 });
// { eventDate: Date object }Nullable and Optional Dates
The codec system preserves nullable and optional semantics:
const Schema = z.object({
requiredDate: z.date(),
optionalDate: z.date().optional(),
nullableDate: z.date().nullable(),
optionalNullableDate: z.date().nullable().optional(),
});
const codec = convexCodec(Schema);
// All variations work correctly
const encoded = codec.encode({
requiredDate: new Date(),
optionalDate: undefined,
nullableDate: null,
// optionalNullableDate omitted
});Arrays of Dates
Arrays and nested arrays are fully supported:
const TimeSeriesSchema = z.object({
timestamps: z.array(z.date()),
nestedDates: z.array(
z.object({
start: z.date(),
end: z.date(),
})
),
});
const codec = convexCodec(TimeSeriesSchema);
// All dates in arrays are convertedCodec System Internals
Codec Registry
The codec system uses a registry pattern for extensibility:
// Built-in codec for Date
const dateCodec = {
check: schema => isDateSchema(schema),
toValidator: () => v.float64(),
fromConvex: value => new Date(value),
toConvex: value => value.getTime(),
};Lookup Order
When processing a schema, the codec system:
- Checks registered custom codecs (via
registerBaseCodec) - Checks built-in Date codec
- Falls back to recursive structural conversion
Performance Considerations
- Codecs are created once and reused
- Encode/decode operations are synchronous
- No schema parsing on each operation
- Minimal overhead for non-Date types
Migration from Manual Conversion
If you've been manually converting between JavaScript types (like Date) and Convex storage formats, this guide shows how to migrate to the codec system.
Before (Manual Conversion)
// Manual: Boilerplate-heavy approach
export const createEvent = mutation({
args: {
title: v.string(),
startDate: v.float64(), // Manually specify timestamp type
endDate: v.float64(),
reminders: v.array(v.float64()), // Array of timestamps
},
handler: async (ctx, args) => {
// Manual conversion if needed for business logic
const startDate = new Date(args.startDate);
const endDate = new Date(args.endDate);
// Validate business rules
if (startDate >= endDate) {
throw new Error('Start date must be before end date');
}
await ctx.db.insert('events', args);
},
});
export const getEvent = query({
handler: async ctx => {
const event = await ctx.db.query('events').first();
if (!event) return null;
// Manual conversion for client consumption
return {
...event,
startDate: new Date(event.startDate),
endDate: new Date(event.endDate),
reminders: event.reminders.map(ts => new Date(ts)),
};
},
});After (With Codecs)
// With codecs: Clean, type-safe, automatic
const EventSchema = z.object({
title: z.string(),
startDate: z.date(),
endDate: z.date(),
reminders: z.array(z.date()),
});
const eventCodec = convexCodec(EventSchema);
export const createEvent = mutation({
args: eventCodec.validator,
handler: async (ctx, args) => {
// args already encoded (dates as timestamps)
// Business logic can decode if needed
const decoded = eventCodec.decode(args);
if (decoded.startDate >= decoded.endDate) {
throw new Error('Start date must be before end date');
}
await ctx.db.insert('events', args);
},
});
export const getEvent = query({
handler: async ctx => {
const event = await ctx.db.query('events').first();
// Automatic decode handles all nested dates
return event ? eventCodec.decode(event) : null;
},
});Benefits of Migration
| Aspect | Manual Approach | Codec Approach | |--------|----------------|----------------| | Type Safety | Manual type alignment | Automatic from Zod schema | | Date Conversion | Manual in each function | Automatic encode/decode | | Nested Dates | Manual recursive handling | Automatic any depth | | Consistency | Easy to miss fields | Schema is single source | | Refactoring | Update every function | Update schema only | | Type Inference | Manual type definitions | Automatic inference |
Step-by-Step Migration
Define your schema with Zod:
const EventSchema = z.object({ title: z.string(), startDate: z.date(), // Use z.date() instead of z.number() endDate: z.date(), });Create the codec:
import { convexCodec } from '@libar-dev/zod-convex-codecs'; const eventCodec = convexCodec(EventSchema);Update mutation args:
// Before args: { title: v.string(), startDate: v.float64() } // After args: eventCodec.validatorUpdate query returns:
// Before return { ...event, startDate: new Date(event.startDate) }; // After return eventCodec.decode(event);
Common Migration Patterns
Pattern 1: Partial schema with pick
const fullCodec = convexCodec(FullEventSchema);
// Create codec for just the date fields
const datesCodec = fullCodec.pick(['startDate', 'endDate']);Pattern 2: Gradual migration
// Keep manual for some fields, use codec for dates
const createEvent = mutation({
args: {
title: v.string(), // Keep manual
...eventCodec.pick(['startDate', 'endDate']).validator, // Migrate dates
},
handler: async (ctx, args) => { /* ... */ },
});Pattern 3: Shared codecs across files
// shared/codecs.ts
export const eventCodec = convexCodec(EventSchema);
export const userCodec = convexCodec(UserSchema);
// convex/events.ts
import { eventCodec } from '../shared/codecs';Comparison Table
| Task | Manual Code | With Codecs |
|------|-------------|-------------|
| Define Date field | v.float64() | z.date() in schema |
| Convert Date → timestamp | date.getTime() | codec.encode() (auto) |
| Convert timestamp → Date | new Date(ts) | codec.decode() (auto) |
| Handle nested dates | Loop/map manually | Automatic |
| Handle arrays of dates | .map(ts => new Date(ts)) | Automatic |
| Handle nullable dates | ts ? new Date(ts) : null | Automatic |
| Partial updates | Manual field selection | codec.pick([...]) |
Type Safety
The codec system maintains full TypeScript type inference:
const Schema = z.object({
name: z.string(),
birthDate: z.date(),
});
const codec = convexCodec(Schema);
// TypeScript knows the types
type EncodedType = ReturnType<typeof codec.encode>;
// { name: string; birthDate: number }
type DecodedType = ReturnType<typeof codec.decode>;
// { name: string; birthDate: Date }Related Packages
@libar-dev/zod-convex-core- Core conversion engine@libar-dev/zod-convex-ids- Type-safe ID validation@libar-dev/zod-convex-builders- Function builders@libar-dev/zod-convex-tables- Table utilities
License
MIT License - See LICENSE file for details.
Copyright (c) 2025 Libar AI
