npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@libar-dev/zod-convex-codecs

v0.1.0

Published

Bidirectional codecs for Date and custom type conversion with Convex

Downloads

48

Readme

@libar-dev/zod-convex-codecs

npm version License: MIT TypeScript

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 Date and 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.0

Peer Dependencies:

  • convex >= 1.30.0 < 2.0.0
  • zod ^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); // true

API 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 fields

Key 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 → removed

Conversion Rules:

  • Date objects → Unix timestamps (milliseconds)
  • undefined values → 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 timestamps

Custom 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 converted

Codec 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:

  1. Checks registered custom codecs (via registerBaseCodec)
  2. Checks built-in Date codec
  3. 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

  1. 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(),
    });
  2. Create the codec:

    import { convexCodec } from '@libar-dev/zod-convex-codecs';
    const eventCodec = convexCodec(EventSchema);
  3. Update mutation args:

    // Before
    args: { title: v.string(), startDate: v.float64() }
    
    // After
    args: eventCodec.validator
  4. Update 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

License

MIT License - See LICENSE file for details.

Copyright (c) 2025 Libar AI