fastify-lor-zod
v0.8.0
Published
A Fastify type provider integrating Zod v4 for schema validation, response serialization, and OpenAPI generation
Downloads
325
Maintainers
Readme
fastify-lor-zod
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. A ground-up rebuild of turkerdev/fastify-type-provider-zod on Zod v4's native APIs — fixes 25+ open issues.
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
safeParsefor ~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, andreply.send()fully typed - 100% test coverage with snapshot parity against
fastify-type-provider-zod - Why "Lor"? -- Son of Zod, here to power your
fastifyschemas.
Table of Contents
- Install
- Quick Start
- Serializer Compilers
- OpenAPI / Swagger
- Typed Plugins
- Error Handling
- Zod v4 Codec Support
- Compatibility
- Contributing
- License
Install
pnpm add fastify-lor-zod
pnpm add -D fastify zod # peer dependencies
pnpm add -D @fastify/swagger # optional, for OpenAPIQuick 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 benchto reproduce, orpnpm bench:lib lor-zodfor this library only.
OpenAPI / Swagger
Integrate with @fastify/swagger for automatic OpenAPI spec generation. transform converts Zod schemas per route, transformObject populates components.schemas from a registry (safe to include even without one):
import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
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,
});
await app.register(swaggerUi, { routePrefix: '/documentation' });- OAS 3.0 and 3.1 support
- Automatic
io: "input"for request schemas,io: "output"for response schemas - Nullable types, discriminated unions, recursive schemas handled correctly
- Nested content types (
application/json,multipart/form-data, etc.) - Response
descriptionpreserved from wrapper objects zodToJsonConfigpassthrough for customz.toJSONSchema()options
Schema Registry
Register schemas with z.globalRegistry or a custom registry to generate $ref-based components.schemas:
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 }),
});Schemas whose input and output shapes diverge (e.g. due to .default(), transforms, or codecs) automatically get {Id}Input variants in components.schemas. No configuration needed.
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);Typed Handlers
Use RouteHandler to define handlers in separate files while preserving Zod type inference:
import type { RouteHandler } from 'fastify-lor-zod';
const schema = {
params: z.object({ id: z.coerce.number() }),
response: { 200: z.object({ name: z.string() }) },
} as const;
const getUser: RouteHandler<typeof schema> = (req) => {
req.params.id; // number
return { name: 'Alice' };
};
app.get('/users/:id', { schema }, getUser);Error Handling
Validation errors are detected with the isRequestValidationError type guard. Serialization errors use instanceof on the ResponseSerializationError class.
import {
isRequestValidationError,
ResponseSerializationError,
} from 'fastify-lor-zod';
app.setErrorHandler((error, request, reply) => {
if (isRequestValidationError(error)) {
// Log input server-side only — may contain sensitive fields
request.log.error({ input: error.input });
reply.code(400).send({
error: 'Validation failed',
issues: error.validation, // FastifySchemaValidationError[]
context: error.validationContext, // 'body' | 'querystring' | 'params' | 'headers'
});
return;
}
if (error instanceof ResponseSerializationError) {
reply.code(500).send({
error: 'Response serialization failed',
code: error.code, // 'ERR_RESPONSE_SERIALIZATION'
method: error.method, // 'GET'
url: error.url, // '/users/42'
httpStatus: error.httpStatus, // '200'
});
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.x | >= 5.8.4 | >= 4.3.6 | >= 9.7.0 (optional) | >= 6.3.0 (optional, for fastSerializerCompiler) | >= 22 |
Migrating from fastify-type-provider-zod
See MIGRATION.md for a step-by-step guide.
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
