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

@kotodayori/zod

v1.0.3

Published

Zod schema validation helpers for @kotodayori/core

Readme

@kotodayori/zod

Zod schema validation helpers for Kotodayori webhook router.

Overview

@kotodayori/zod adds runtime validation to your webhook handlers using Zod schemas. Validate event payloads, enforce data contracts, and catch malformed webhooks before they reach your business logic.

Installation

npm install @kotodayori/zod @kotodayori/core zod
# or
pnpm add @kotodayori/zod @kotodayori/core zod
# or
yarn add @kotodayori/zod @kotodayori/core zod

Note: Both @kotodayori/core and zod are peer dependencies and must be installed separately. Requires Zod v4.0.0 or higher.

Features

  • Runtime Validation: Validate webhook payloads at runtime with Zod schemas
  • Type-Safe: Full TypeScript support with automatic type inference
  • Flexible: Validate at the verifier level or middleware level
  • Schema Registry: Centralized schema management for event types
  • Error Handling: Detailed validation errors with helpful messages
  • Transform Support: Apply Zod transformations and defaults to events
  • Unknown Event Handling: Optionally reject or allow unregistered event types

Quick Start

Basic Validation with Middleware

import { WebhookRouter } from '@kotodayori/core';
import { SchemaRegistry, withValidation, createEventSchema } from '@kotodayori/zod';
import { z } from 'zod';

// Define schemas for your events
const issueOpenedSchema = createEventSchema('issue.opened', z.object({
  id: z.number(),
  title: z.string(),
  body: z.string().optional(),
  labels: z.array(z.string()),
}));

const issueClosedSchema = createEventSchema('issue.closed', z.object({
  id: z.number(),
  closedAt: z.string().datetime(),
}));

// Create a schema registry
const registry = new SchemaRegistry()
  .register('issue.opened', issueOpenedSchema)
  .register('issue.closed', issueClosedSchema);

// Create router with validation middleware
const router = new WebhookRouter();

router.use(withValidation(registry));

router.on('issue.opened', async (event) => {
  // event.data.object is validated and typed!
  const issue = event.data.object;
  console.log(`Issue #${issue.id}: ${issue.title}`);
});

Validation at Verifier Level

import { createZodVerifier } from '@kotodayori/zod';

const verifier = createZodVerifier({
  verifier: createMyCustomVerifier(secret),
  registry,
  allowUnknownEvents: false, // Reject events without schemas
});

app.post('/webhook', honoAdapter(router, { verifier }));

API Reference

createEventSchema

Create a Zod schema for a webhook event.

function createEventSchema<TType, TDataObject>(
  type: TType,
  dataObjectSchema: TDataObject
): ZodSchema

Parameters:

  • type - The event type string (e.g., 'issue.opened')
  • dataObjectSchema - Zod schema for the data.object property

Returns: A Zod schema for the complete event

Example:

const paymentSchema = createEventSchema('payment.succeeded', z.object({
  id: z.string(),
  amount: z.number().int().positive(),
  currency: z.string().length(3),
  status: z.literal('succeeded'),
}));

type PaymentEvent = z.infer<typeof paymentSchema>;
// {
//   id: string;
//   type: 'payment.succeeded';
//   data: {
//     object: {
//       id: string;
//       amount: number;
//       currency: string;
//       status: 'succeeded';
//     };
//   };
// }

defineEvent

Helper for creating event schema definitions.

function defineEvent<TType, TDataObject>(
  type: TType,
  dataObjectSchema: TDataObject
): EventSchemaDefinition

Example:

const issueOpened = defineEvent('issue.opened', z.object({
  id: z.number(),
  title: z.string(),
}));

const issueClosed = defineEvent('issue.closed', z.object({
  id: z.number(),
}));

const schemas = { issueOpened, issueClosed };
type EventMap = InferEventMap<typeof schemas>;

SchemaRegistry

Centralized registry for event schemas.

class SchemaRegistry<TEventMap>

Methods

register(type, schema)

Register a schema for an event type.

registry.register('payment.succeeded', paymentSchema);
registerAll(definitions)

Register multiple event definitions at once.

const schemas = {
  paymentSucceeded: defineEvent('payment.succeeded', z.object({ ... })),
  paymentFailed: defineEvent('payment.failed', z.object({ ... })),
};

registry.registerAll(schemas);
validate(event)

Validate an event against its registered schema.

const validatedEvent = registry.validate(event);
safeParse(event)

Safely validate an event, returning a result object.

const result = registry.safeParse(event);
if (result.success) {
  console.log('Valid:', result.data);
} else {
  console.error('Invalid:', result.error);
}

withValidation

Create a validation middleware.

function withValidation<TEventMap>(
  registry: SchemaRegistry<TEventMap>,
  options?: ValidationMiddlewareOptions
): Middleware

Options:

interface ValidationMiddlewareOptions {
  /**
   * Whether to allow events without registered schemas
   * @default true
   */
  allowUnknownEvents?: boolean;

  /**
   * Custom error handler
   */
  onError?: (error: WebhookValidationError | UnknownEventTypeError) => void;
}

Example:

router.use(withValidation(registry, {
  allowUnknownEvents: false, // Reject unknown events
  onError: (error) => {
    console.error('Validation failed:', error);
  },
}));

createZodVerifier

Wrap a verifier with Zod validation.

function createZodVerifier<T>(
  options: ZodVerifierOptions<T>
): Verifier<T>

Options:

interface ZodVerifierOptions<T> {
  verifier: Verifier<T>;
  registry: SchemaRegistry;
  allowUnknownEvents?: boolean;
}

Example:

const verifier = createZodVerifier({
  verifier: createStripeVerifier(stripe, secret),
  registry,
  allowUnknownEvents: false,
});

Usage Examples

GitHub Webhooks

import { WebhookRouter } from '@kotodayori/core';
import { SchemaRegistry, defineEvent, withValidation, InferEventMap } from '@kotodayori/zod';
import { z } from 'zod';

// Define GitHub event schemas
const pushEvent = defineEvent('push', z.object({
  ref: z.string(),
  commits: z.array(z.object({
    id: z.string(),
    message: z.string(),
    author: z.object({
      name: z.string(),
      email: z.string().email(),
    }),
  })),
}));

const issueEvent = defineEvent('issues', z.object({
  action: z.enum(['opened', 'closed', 'reopened']),
  issue: z.object({
    number: z.number(),
    title: z.string(),
    state: z.enum(['open', 'closed']),
  }),
}));

const schemas = { pushEvent, issueEvent };
type GitHubEventMap = InferEventMap<typeof schemas>;

// Create registry and router
const registry = new SchemaRegistry<GitHubEventMap>()
  .registerAll(schemas);

const router = new WebhookRouter<GitHubEventMap>();

router.use(withValidation(registry, {
  allowUnknownEvents: false, // Only accept registered events
}));

router.on('push', async (event) => {
  const { ref, commits } = event.data.object;
  console.log(`Push to ${ref}: ${commits.length} commits`);
});

router.on('issues', async (event) => {
  const { action, issue } = event.data.object;
  console.log(`Issue #${issue.number} ${action}`);
});

Data Transformations

Zod schemas can transform data:

const userCreatedSchema = createEventSchema('user.created', z.object({
  id: z.string(),
  email: z.string().email().toLowerCase(), // Transform to lowercase
  createdAt: z.string().datetime().transform(s => new Date(s)), // Transform to Date
  metadata: z.record(z.string()).default({}), // Default value
  age: z.number().int().positive().optional(),
}));

registry.register('user.created', userCreatedSchema);

router.use(withValidation(registry));

router.on('user.created', async (event) => {
  // event.data.object.email is lowercase
  // event.data.object.createdAt is a Date object
  // event.data.object.metadata exists even if not in payload
  const user = event.data.object;
  console.log(user.createdAt.toISOString());
});

Strict Validation

Prevent extra fields and enforce exact schemas:

const strictSchema = createEventSchema('order.created', z.object({
  id: z.string(),
  total: z.number(),
  items: z.array(z.object({
    sku: z.string(),
    quantity: z.number(),
  })),
}).strict()); // Reject unknown fields

registry.register('order.created', strictSchema);

Conditional Validation

Different schemas based on event data:

const notificationSchema = createEventSchema('notification', z.object({
  type: z.enum(['email', 'sms', 'push']),
  recipient: z.string(),
  payload: z.unknown(),
}).refine((data) => {
  if (data.type === 'email') {
    return z.object({
      subject: z.string(),
      body: z.string(),
    }).safeParse(data.payload).success;
  }
  if (data.type === 'sms') {
    return z.object({
      message: z.string().max(160),
    }).safeParse(data.payload).success;
  }
  return true;
}, 'Invalid payload for notification type'));

Error Handling

WebhookValidationError

Thrown when event validation fails:

import { WebhookValidationError } from '@kotodayori/zod';

router.use(withValidation(registry, {
  onError: (error) => {
    if (error instanceof WebhookValidationError) {
      console.error(`Validation failed for ${error.eventType}`);
      console.error('Zod errors:', error.zodError.errors);
    }
  },
}));

UnknownEventTypeError

Thrown when allowUnknownEvents: false and an unregistered event is received:

import { UnknownEventTypeError } from '@kotodayori/zod';

router.use(withValidation(registry, {
  allowUnknownEvents: false,
  onError: (error) => {
    if (error instanceof UnknownEventTypeError) {
      console.error(`Unknown event type: ${error.eventType}`);
    }
  },
}));

Advanced Patterns

Partial Validation

Validate only specific fields:

const partialSchema = createEventSchema('user.updated', z.object({
  id: z.string(),
  email: z.string().email().optional(),
  name: z.string().optional(),
  age: z.number().optional(),
}).partial().required({ id: true })); // Only id is required

Nested Object Validation

const orderSchema = createEventSchema('order.placed', z.object({
  orderId: z.string(),
  customer: z.object({
    id: z.string(),
    email: z.string().email(),
    address: z.object({
      street: z.string(),
      city: z.string(),
      zipCode: z.string().regex(/^\d{5}$/),
    }),
  }),
  items: z.array(z.object({
    productId: z.string(),
    quantity: z.number().int().positive(),
    price: z.number().positive(),
  })).min(1),
  total: z.number().positive(),
}));

Custom Validators

Add custom validation logic:

const priceSchema = z.object({
  amount: z.number(),
  currency: z.string(),
}).refine(
  (data) => {
    // Custom validation: USD amounts must be >= 0.50
    if (data.currency === 'USD' && data.amount < 50) {
      return false;
    }
    return true;
  },
  {
    message: 'USD amounts must be at least $0.50',
    path: ['amount'],
  }
);

Testing

Unit Testing with Zod Validation

import { describe, it, expect } from 'vitest';
import { SchemaRegistry, createEventSchema } from '@kotodayori/zod';
import { z } from 'zod';

describe('Event validation', () => {
  it('validates correct events', () => {
    const schema = createEventSchema('test.event', z.object({
      value: z.number(),
    }));

    const registry = new SchemaRegistry().register('test.event', schema);

    const event = {
      id: '123',
      type: 'test.event',
      data: { object: { value: 42 } },
    };

    expect(() => registry.validate(event)).not.toThrow();
  });

  it('rejects invalid events', () => {
    const schema = createEventSchema('test.event', z.object({
      value: z.number(),
    }));

    const registry = new SchemaRegistry().register('test.event', schema);

    const event = {
      id: '123',
      type: 'test.event',
      data: { object: { value: 'not-a-number' } },
    };

    expect(() => registry.validate(event)).toThrow();
  });
});

Migration from TypeScript-Only Validation

If you currently rely only on TypeScript types:

// Before: TypeScript only (no runtime validation)
router.on('payment.succeeded', async (event) => {
  const amount = event.data.object.amount; // Might be undefined at runtime!
});

// After: Runtime validation with Zod
const paymentSchema = createEventSchema('payment.succeeded', z.object({
  amount: z.number(),
}));

registry.register('payment.succeeded', paymentSchema);
router.use(withValidation(registry));

router.on('payment.succeeded', async (event) => {
  const amount = event.data.object.amount; // Guaranteed to be a number
});

Requirements

  • Node.js >= 18
  • TypeScript >= 5.3
  • Zod >= 4.0.0

Related Packages

Documentation

For more examples and guides, see the main documentation.

License

MIT