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

fastify-lor-zod

v0.1.7

Published

A Fastify type provider integrating Zod for schema validation and type-safe route definitions

Readme

fastify-lor-zod

CI npm version license TypeScript

Note -- Pre-1.0: minor versions may include breaking changes. Pin your version and check the changelog before upgrading.

A Fastify type provider for Zod v4 with full OpenAPI support.

Built with good vibes for Fastify v5 and Zod v4. Fixes issues from turkerdev/fastify-type-provider-zod.

Why fastify-lor-zod?

  • Zod v4 native -- uses safeEncode, toJSONSchema, codecs, and registries directly
  • Smart serializer -- auto-detects codecs at compile time; falls back to safeParse for ~15% faster non-codec schemas
  • Complete OpenAPI -- all HTTP parts, nullable types, discriminated unions, recursive schemas, content types
  • Type-safe end-to-end -- req.body, req.params, req.query, req.headers, and reply.send() fully typed
  • 100% test coverage -- 127 tests including snapshot parity with fastify-type-provider-zod
  • Why "Lor"? -- Son of Zod, here to power your fastify schemas.

Table of Contents

Install

pnpm add fastify-lor-zod
pnpm add -D fastify zod                    # peer dependencies
pnpm add -D @fastify/swagger               # optional, for OpenAPI

Quick Start

import Fastify from 'fastify';
import { z } from 'zod';
import {
  validatorCompiler,
  serializerCompiler,
  type FastifyLorZodTypeProvider,
} from 'fastify-lor-zod';

const app = Fastify();
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);

app.withTypeProvider<FastifyLorZodTypeProvider>().get(
  '/user/:id',
  {
    schema: {
      params: z.object({ id: z.coerce.number() }),
      response: {
        200: z.object({ id: z.number(), name: z.string() }),
      },
    },
  },
  (req) => ({ id: req.params.id, name: 'Alice' }),
  //              ^ fully typed as number
);

app.listen({ port: 3000 });

Serializer Compilers

Three strategies for different trade-offs:

| Compiler | Validates | Codecs | Speed | Use when | |----------|-----------|--------|-------|----------| | serializerCompiler | Yes | Auto-detect | Fastest validating | Recommended default -- uses safeParse for plain schemas, safeEncode only when codecs are present | | parseSerializerCompiler | Yes | No | Same as above | Explicit opt-in to always use safeParse | | fastSerializerCompiler | No | No | Fastest overall | You trust your handlers and want maximum throughput |

import {
  serializerCompiler,         // default: auto-detects codecs, picks safeParse or safeEncode
  parseSerializerCompiler,    // always z.safeParse + JSON.stringify
  fastSerializerCompiler,     // fast-json-stringify, no validation
} from 'fastify-lor-zod';

app.setSerializerCompiler(serializerCompiler);

createSerializerCompiler and createParseSerializerCompiler each accept a replacer option for JSON.stringify. createFastSerializerCompiler takes no options — fast-json-stringify pre-compiles the serializer at route registration time and does not use JSON.stringify.

Benchmarks

Serialization throughput (ops/sec, higher is better):

| Scenario | lor-zod | lor-zod (parse) | lor-zod (fast) | type-provider-zod | zod-openapi | |----------|---------|-----------------|----------------|-------------------|-------------| | Simple object | 278K | 287K | 610K | 291K | 271K | | Simple object + date codec | 142K | Unsupported | 211K | Unsupported | Unsupported | | Nested (10 items) | 33K | 34K | 86K | 34K | 30K | | Nested + money codec | 29K | Unsupported | 90K | Unsupported | Unsupported | | Discriminated union | 499K | 487K | 651K | 505K | 316K | | Recursive tree | 407K | 383K | 1.13M | 397K | 438K |

For non-codec schemas, serializerCompiler auto-detects and matches parseSerializerCompiler speed. For codec schemas, it automatically uses safeEncode.

Validation throughput (all libraries are within ~5% of each other):

| Scenario | lor-zod | type-provider-zod | zod-openapi | |----------|---------|-------------------|-------------| | Simple object | 386K | 360K | 366K | | Nested (10 items) | 57K | 57K | 58K | | Discriminated union | 996K | 946K | 933K | | Recursive tree | 819K | 805K | 758K |

Measured on Apple M-series, Node.js 24, Zod 4.3.6. Run pnpm bench to reproduce, or pnpm bench:lib lor-zod for this library only.

OpenAPI / Swagger

The library provides two @fastify/swagger hooks: transform (converts Zod schemas per route) and transformObject (populates components.schemas from a registry). Which ones you need depends on whether you use a schema registry:

  • No registered schemastransform alone is sufficient. All schemas are inlined.
  • With registered schemas (via z.globalRegistry or a custom registry) — use both. transform emits $refs for registered schemas, transformObject provides the component definitions they point to.

Basic Setup (no registry)

import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
import { jsonSchemaTransform } from 'fastify-lor-zod';

await app.register(swagger, {
  openapi: {
    openapi: '3.0.3',
    info: { title: 'My API', version: '1.0.0' },
  },
  transform: jsonSchemaTransform,
});

await app.register(swaggerUi, { routePrefix: '/documentation' });

With a Registry

When using z.globalRegistry or a custom registry, add transformObject to populate components.schemas with $ref-based definitions:

import { jsonSchemaTransform, jsonSchemaTransformObject } from 'fastify-lor-zod';

await app.register(swagger, {
  openapi: {
    openapi: '3.0.3',
    info: { title: 'My API', version: '1.0.0' },
  },
  transform: jsonSchemaTransform,
  transformObject: jsonSchemaTransformObject,
});

OpenAPI Features

  • OAS 3.0 and 3.1 support
  • Automatic io: "input" for request schemas, io: "output" for response schemas
  • z.registry() and z.globalRegistry resolve to $ref components
  • Nullable types, discriminated unions, recursive schemas handled correctly
  • Nested content types in body and response (application/json, multipart/form-data, etc.)
  • Response description preserved from wrapper objects
  • zodToJsonConfig passthrough for custom z.toJSONSchema() options

Custom Schema Registry

For a custom registry (instead of z.globalRegistry), use the factory functions with shared options:

import { z } from 'zod';
import { createJsonSchemaTransforms } from 'fastify-lor-zod';

const registry = z.registry<{ id: string }>();
const UserSchema = z.object({ id: z.number(), name: z.string() });
registry.add(UserSchema, { id: 'User' });

await app.register(swagger, {
  openapi: { openapi: '3.0.3', info: { title: 'My API', version: '1.0.0' } },
  ...createJsonSchemaTransforms({ schemaRegistry: registry }),
});

The individual factories are also available — useful when you need different options per function or want to pass pre-computed divergentIds:

import {
  createJsonSchemaTransform,
  createJsonSchemaTransformObject,
} from 'fastify-lor-zod';

const opts = { schemaRegistry: registry };
await app.register(swagger, {
  openapi: { openapi: '3.0.3', info: { title: 'My API', version: '1.0.0' } },
  transform: createJsonSchemaTransform(opts),
  transformObject: createJsonSchemaTransformObject(opts),
});

Input/Output Schema Variants

Zod schemas used as request bodies are processed with io: "input", while response schemas use io: "output". For most schemas these are identical, but schemas with transforms, codecs, or defaults can produce different input and output shapes.

Input variants are auto-detected — when a registered schema's input and output JSON Schema representations differ, an {Id}Input component is automatically generated alongside the output variant. No configuration needed.

// CreateUserSchema has role: z.string().default('user')
// Input shape: role is optional. Output shape: role is always present.
registry.add(CreateUserSchema, { id: 'CreateUser' });

await app.register(swagger, {
  openapi: { openapi: '3.0.3', info: { title: 'My API', version: '1.0.0' } },
  ...createJsonSchemaTransforms({ schemaRegistry: registry }),
});

// components.schemas will contain both (auto-detected):
// CreateUser      — output shape (role required)
// CreateUserInput — input shape  (role optional, with default)
// requestBody $ref → #/components/schemas/CreateUserInput
// response    $ref → #/components/schemas/CreateUser

Typed Plugins

import type { FastifyPluginAsyncZod } from 'fastify-lor-zod';

const usersPlugin: FastifyPluginAsyncZod = async (app) => {
  app.get(
    '/users',
    {
      schema: {
        response: { 200: z.array(UserSchema) },
      },
    },
    () => [{ id: 1, name: 'Alice' }],
  );
};

await app.register(usersPlugin);

Error Handling

Both error classes use modern ES2022+ patterns with instanceof support, a stable code property for programmatic matching, and cause chaining via ErrorOptions.

import {
  RequestValidationError,
  ResponseSerializationError,
} from 'fastify-lor-zod';

app.setErrorHandler((error, request, reply) => {
  if (error instanceof RequestValidationError) {
    reply.code(400).send({
      error: 'Validation failed',
      issues: error.validation,
    });
    return;
  }

  if (error instanceof ResponseSerializationError) {
    reply.code(500).send({
      error: 'Response serialization failed',
      code: error.code,    // 'ERR_RESPONSE_SERIALIZATION'
      method: error.method,
      url: error.url,
    });
    return;
  }

  reply.send(error);
});

Zod v4 Codec Support

Zod v4 codecs encode domain types to wire format. The default serializer handles this automatically:

const dateCodec = z.codec(z.iso.datetime(), z.date(), {
  decode: (iso: string) => new Date(iso),
  encode: (date: Date) => date.toISOString(),
});

app.get(
  '/event',
  {
    schema: {
      response: {
        200: z.object({ startsAt: dateCodec }),
      },
    },
  },
  () => ({ startsAt: new Date() }),
  // Response: { "startsAt": "2025-06-15T10:00:00.000Z" }
);

Compatibility

| fastify-lor-zod | Fastify | Zod | @fastify/swagger | fast-json-stringify | Node.js | |-----------------|---------|-----|------------------|---------------------|---------| | 0.1.x | >= 5.8.4 | >= 4.3.6 | >= 9.7.0 (optional) | >= 6.3.0 (optional, for fastSerializerCompiler) | >= 24 |

Migrating from fastify-type-provider-zod

See MIGRATION.md for a step-by-step guide.

Issues Addressed

Fixes issues from turkerdev/fastify-type-provider-zod:

| Issue | Description | |-------|-------------| | #244 | params/querystring missing from OpenAPI | | #233 | Cannot tweak toJSONSchema options | | #214 | Input schema variants leak into components | | #211 | Serializer should use .encode() for Zod v4 | | #210 | Schema definitions ignored | | #209 | Cannot modify headers after validation | | #208 | transform() loses response type info | | #195 | anyOf with 3+ items broken | | #193 | Nullable types converted incorrectly | | #192 | z.null in union generates invalid JSON Schema | | #178 | Multi-content schemas not supported | | #170 | components.schemas not populated | | #158 | .default(null) crashes | | #155 | .optional().default() querystring fails | | #148 | Optional fields treated as required | | #132 | Body/response content types not handled | | #71 | z.readonly() not supported | | #47 | Response description ignored |

Contributing

git clone https://github.com/drudolf/fastify-lor-zod.git
cd fastify-lor-zod
pnpm install

| Command | Description | |---------|-------------| | pnpm test | Run tests | | pnpm test:coverage | Run tests with 100% coverage enforcement | | pnpm check | Lint + format (Biome) | | pnpm typecheck | Type-check with tsc --noEmit | | pnpm knip | Detect unused exports and dependencies | | pnpm bench | Run benchmarks against all type providers | | pnpm bench:lib <filter> | Run benchmarks for a single library (e.g. lor-zod, type-provider, zod-openapi) | | pnpm build | Build the project (ESM and CJS) |

Tests follow a spec-first workflow -- see test-spec.md for the full test matrix and CLAUDE.md for project conventions.

License

MIT