fastify-type-provider-zod

repository·main·Indexed 20 days ago

https://github.com/turkerdev/fastify-type-provider-zod

A Zod Type Provider for Fastify@5 that enables type-safe request validation, response serialization, and automatic OpenAPI documentation generation. It provides a ZodTypeProvider for TypeScript type inference, validator and serializer compilers, and integration utilities for @fastify/swagger via jsonSchemaTransform and jsonSchemaTransformObject.

Tokens
6.6K
Snippets
21
Records
23
Agent score
69%

What's inside fastify-type-provider-zod

  1. Create schema references (refs) for OpenAPI

    main

    To avoid duplicating schemas in your OpenAPI document and instead use $ref, register your schemas with the global Zod registry using z.globalRegistry.add(schema, { id: 'Name' }). Then, configure @fastify/swagger with both transform: jsonSchemaTransform and transformObject: jsonSchemaTransformObject.

    import fastifySwagger from '@fastify/swagger';
    import fastifySwaggerUI from '@fastify/swagger-ui';
    import fastify from 'fastify';
    import { z } from 'zod/v4';
    import type { ZodTypeProvider } from 'fastify-type-provider-zod';
    import {
      jsonSchemaTransformObject,
      jsonSchemaTransform,
      serializerCompiler,
      validatorCompiler,
    } from 'fastify-type-provider-zod';
    
    const USER_SCHEMA = z.object({
      id: z.number().int().positive(),
      name: z.string().describe('The name of the user'),
    });
    
    // Register schema with a global ID for referencing
    z.globalRegistry.add(USER_SCHEMA, { id: 'User' });
    
    const app = fastify();
    app.setValidatorCompiler(validatorCompiler);
    app.setSerializerCompiler(serializerCompiler);
    
    app.register(fastifySwagger, {
      openapi: {
        info: { title: 'SampleApi', version: '1.0.0' },
        servers: [],
      },
      transform: jsonSchemaTransform,
      transformObject: jsonSchemaTransformObject,
    });
    
    app.register(fastifySwaggerUI, { routePrefix: '/documentation' });
    
    app.after(() => {
      app.withTypeProvider<ZodTypeProvider>().route({
        method: 'GET',
        url: '/users',
        schema: {
          response: {
            200: USER_SCHEMA.array(),
          },
        },
        handler: (req, res) => {
          res.send([]);
        },
      });
    });
  2. Integrate with @fastify/swagger

    main

    To generate OpenAPI documentation from your Zod schemas, register @fastify/swagger and use the jsonSchemaTransform provided by this library. This allows the Swagger UI to correctly interpret your Zod-defined schemas.

    import fastify from 'fastify';
    import fastifySwagger from '@fastify/swagger';
    import fastifySwaggerUI from '@fastify/swagger-ui';
    import { z } from 'zod/v4';
    import type { ZodTypeProvider } from 'fastify-type-provider-zod';
    import {
      jsonSchemaTransform,
      serializerCompiler,
      validatorCompiler,
    } from 'fastify-type-provider-zod';
    
    const app = fastify();
    app.setValidatorCompiler(validatorCompiler);
    app.setSerializerCompiler(serializerCompiler);
    
    app.register(fastifySwagger, {
      openapi: {
        info: {
          title: 'SampleApi',
          description: 'Sample backend service',
          version: '1.0.0',
        },
        servers: [],
      },
      transform: jsonSchemaTransform,
    });
    
    app.register(fastifySwaggerUI, {
      routePrefix: '/documentation',
    });
    
    // ... routes using app.withTypeProvider<ZodTypeProvider>()
  3. Install and basic setup of Fastify Type Provider Zod

    main

    To use Zod for schema validation and serialization in Fastify, you must register the validatorCompiler and serializerCompiler. Use app.withTypeProvider<ZodTypeProvider>() to enable type safety for your routes.

    Important (v7+): Response serialization is now based on z.output<T> instead of z.input<T>. Fastify serializers expect the post-transformation output type of your Zod schemas.

    import Fastify from 'fastify';
    import type { ZodTypeProvider } from 'fastify-type-provider-zod';
    import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
    import { z } from 'zod/v4';
    
    const app = Fastify();
    
    // Add schema validator and serializer
    app.setValidatorCompiler(validatorCompiler);
    app.setSerializerCompiler(serializerCompiler);
    
    app.withTypeProvider<ZodTypeProvider>().route({
      method: 'GET',
      url: '/',
      schema: {
        querystring: z.object({
          name: z.string().min(4),
        }),
        response: {
          200: z.string(),
        },
      },
      handler: (req, res) => {
        res.send(req.query.name);
      },
    });
    
    app.listen({ port: 4949 });
  4. Define multiple response content types

    main

    You can define multiple content types for a single response status following the OpenAPI 3.x format. jsonSchemaTransform will handle these correctly. To select the appropriate schema during execution, set the Content-Type header in your handler.

    import { z } from 'zod/v4';
    import type { ZodTypeProvider } from 'fastify-type-provider-zod';
    
    app.withTypeProvider<ZodTypeProvider>().route({
      method: 'GET',
      url: '/items',
      schema: {
        response: {
          200: {
            description: 'Successful response',
            content: {
              'application/json': { schema: z.object({ id: z.number(), name: z.string() }) },
              'application/vnd.v1+json': { schema: z.array(z.object({ id: z.number(), name: z.string() })) },
            },
          },
          default: {
            content: {
              '*/*': { schema: z.object({ message: z.string() }) },
            },
          },
        },
      },
      handler: (req, res) => {
        // Set header to select which schema validates the response
        res.header('Content-Type', 'application/vnd.v1+json');
        res.send([{ id: 1, name: 'item' }]);
      },
    });
  5. Define empty body responses

    main

    To define a response with no body (e.g., for a 204 No Content status), use z.undefined(). This ensures res.send() is correctly typed and the OpenAPI schema accurately reflects the empty body.

    import { z } from 'zod/v4';
    import type { ZodTypeProvider } from 'fastify-type-provider-zod';
    
    app.withTypeProvider<ZodTypeProvider>().route({
      method: 'DELETE',
      url: '/resource',
      schema: {
        response: {
          204: z.undefined().describe('Resource deleted'),
        },
      },
      handler: (_req, res) => {
        res.status(204).send();
      },
    });
  6. Create a Fastify plugin with Zod type provider

    main

    When creating a plugin that uses Zod, use the FastifyPluginAsyncZod type to ensure the fastify instance passed to the plugin is correctly typed for use with withTypeProvider.

    import { z } from 'zod/v4';
    import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
    
    const plugin: FastifyPluginAsyncZod = async function (fastify, _opts) {
      fastify.route({
        method: 'GET',
        url: '/',
        schema: {
          querystring: z.object({
            name: z.string().min(4),
          }),
          response: {
            200: z.string(),
          },
        },
        handler: (req, res) => {
          res.send(req.query.name);
        },
      });
    };
  7. Customize the serializer with a replacer function

    main

    You can create a custom serializer compiler using createSerializerCompiler to provide a replacer function. This is useful for custom transformations, such as formatting Date objects during serialization.

    import Fastify from 'fastify';
    import { createSerializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
    
    const app = Fastify();
    
    const replacer = function (key, value) {
      if (this[key] instanceof Date) {
        return { _date: value.toISOString() };
      }
      return value;
    };
    
    // Create a custom serializer compiler
    const customSerializerCompiler = createSerializerCompiler({ replacer });
    
    // Add schema validator and serializer
    app.setValidatorCompiler(validatorCompiler);
    app.setSerializerCompiler(customSerializerCompiler);
    
    // ...
    
    app.listen({ port: 4949 });
  8. Handle Zod validation and serialization errors

    main

    Use hasZodFastifySchemaValidationErrors to detect request validation errors and isResponseSerializationError to detect errors where the response does not match the defined Zod schema. This allows you to return structured error responses.

    import { hasZodFastifySchemaValidationErrors, isResponseSerializationError } from 'fastify-type-provider-zod';
    
    fastifyApp.setErrorHandler((err, req, reply) => {
      if (hasZodFastifySchemaValidationErrors(err)) {
        return reply.code(400).send({
          error: 'Response Validation Error',
          message: "Request doesn't match the schema",
          statusCode: 400,
          details: {
            issues: err.validation,
            method: req.method,
            url: req.url,
          },
        });
      }
    
      if (isResponseSerializationError(err)) {
        return reply.code(500).send({
          error: 'Internal Server Error',
          message: "Response doesn't match the schema",
          statusCode: 500,
          details: {
            issues: err.cause.issues,
            method: err.method,
            url: err.url,
          },
        });
      }
    });
  9. Specify OpenAPI target for JSON Schema transformation

    main

    Use createJsonSchemaTransform with the zodToJsonConfig.target option to specify which JSON Schema version to target for OpenAPI compatibility.

    • Use openapi-3.0 for OpenAPI 3.0.x documents.
    • Use draft-2020-12 for OpenAPI 3.1+ documents.
    import { createJsonSchemaTransform } from "fastify-type-provider-zod";
    
    // For OpenAPI 3.0.x compatibility
    const transform = createJsonSchemaTransform({
      zodToJsonConfig: { target: "openapi-3.0" },
    });
    
    // For OpenAPI 3.1+
    const transform = createJsonSchemaTransform({
      zodToJsonConfig: { target: "draft-2020-12" },
    });
  10. Convert Zod errors to Fastify validation errors

    main

    If you are manually handling Zod errors and want to transform them into a format compatible with Fastify's schema validation error structure, use createValidationError.

    This function maps a $ZodError into an array of ZodFastifySchemaValidationError objects, converting Zod's path into a standard JSON pointer instancePath (e.g., /user/name) and mapping other fields like keyword and message.

    import { createValidationError } from 'fastify-type-provider-zod'
    import { z } from 'zod'
    
    try {
      // manual zod validation
      someSchema.parse(data)
    } catch (err) {
      if (err instanceof z.ZodError) {
        const fastifyErrors = createValidationError(err)
        // use fastifyErrors to build your response
      }
    }
  11. Use validatorCompiler for Zod validation

    main

    The validatorCompiler is a FastifySchemaCompiler that uses Zod's safeParse to validate incoming request data (body, querystring, params, headers). If validation fails, it returns a structured error created via createValidationError.

    import { validatorCompiler } from 'fastify-type-provider-zod';
    
    fastify.setValidatorCompiler(validatorCompiler);
  12. Generate I/O registries with generateIORegistries

    main

    The generateIORegistries function takes a base $ZodRegistry and derives two separate registries: one for inputs and one for outputs. It automatically appends the suffix Input to schema IDs in the input registry, while keeping output IDs as they are.

    Note on Collisions: If an input schema ID (after appending Input) matches an existing output schema ID, the function will throw an error to prevent component name collisions in the generated documentation.

    import { $ZodRegistry } from 'zod/v4/core';
    import { generateIORegistries } from './registry';
    
    const baseRegistry = new $ZodRegistry<SchemaRegistryMeta>();
    // ... add schemas to baseRegistry ...
    
    const { inputRegistry, outputRegistry } = generateIORegistries(baseRegistry);