import type { FastifyPluginAsync, FastifyPluginCallback, FastifyPluginOptions, FastifySchema, FastifySchemaCompiler, FastifyTypeProvider, RawServerBase, RawServerDefault, } from 'fastify'; import type { FastifySerializerCompiler } from 'fastify/types/schema'; import type { ZodAny, ZodTypeAny, z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type FreeformRecord = Record; const defaultSkipList = [ '/documentation/', '/documentation/initOAuth', '/documentation/json', '/documentation/uiConfig', '/documentation/yaml', '/documentation/*', '/documentation/static/*', ]; export interface ZodTypeProvider extends FastifyTypeProvider { output: this['input'] extends ZodTypeAny ? z.infer : never; } interface Schema extends FastifySchema { hide?: boolean; } const zodToJsonSchemaOptions = { target: 'openApi3', $refStrategy: 'none', } as const; export const createJsonSchemaTransform = ({ skipList }: { skipList: readonly string[] }) => { return ({ schema, url }: { schema: Schema; url: string }) => { if (!schema) { return { schema, url, }; } const { response, headers, querystring, body, params, hide, ...rest } = schema; const transformed: FreeformRecord = {}; if (skipList.includes(url) || hide) { transformed.hide = true; return { schema: transformed, url }; } const zodSchemas: FreeformRecord = { headers, querystring, body, params }; for (const prop in zodSchemas) { const zodSchema = zodSchemas[prop]; if (zodSchema) { transformed[prop] = zodToJsonSchema(zodSchema, zodToJsonSchemaOptions); } } if (response) { transformed.response = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any for (const prop in response as any) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const schema = resolveSchema((response as any)[prop]); const transformedResponse = zodToJsonSchema( // eslint-disable-next-line @typescript-eslint/no-explicit-any schema as any, zodToJsonSchemaOptions, ); transformed.response[prop] = transformedResponse; } } for (const prop in rest) { const meta = rest[prop as keyof typeof rest]; if (meta) { transformed[prop] = meta; } } return { schema: transformed, url }; }; }; export const jsonSchemaTransform = createJsonSchemaTransform({ skipList: defaultSkipList, }); export const validatorCompiler: FastifySchemaCompiler = ({ schema }) => // eslint-disable-next-line @typescript-eslint/no-explicit-any (data): any => { try { return { value: schema.parse(data) }; } catch (error) { return { error }; } }; // eslint-disable-next-line @typescript-eslint/no-explicit-any function hasOwnProperty(obj: T, prop: K): obj is T & Record { return Object.prototype.hasOwnProperty.call(obj, prop); } function resolveSchema(maybeSchema: ZodAny | { properties: ZodAny }): Pick { if (hasOwnProperty(maybeSchema, 'safeParse')) { return maybeSchema; } if (hasOwnProperty(maybeSchema, 'properties')) { return maybeSchema.properties; } throw new Error(`Invalid schema passed: ${JSON.stringify(maybeSchema)}`); } export class ResponseValidationError extends Error { public details: FreeformRecord; constructor(validationResult: FreeformRecord) { super("Response doesn't match the schema"); this.name = 'ResponseValidationError'; this.details = validationResult.error; } } export const serializerCompiler: FastifySerializerCompiler = ({ schema: maybeSchema }) => (data) => { const schema: Pick = resolveSchema(maybeSchema); const result = schema.safeParse(data); if (result.success) { return JSON.stringify(result.data); } throw new ResponseValidationError(result); }; /** * FastifyPluginCallbackZod with Zod automatic type inference * * @example * ```typescript * import { FastifyPluginCallbackZod } fromg "fastify-type-provider-zod" * * const plugin: FastifyPluginCallbackZod = (fastify, options, done) => { * done() * } * ``` */ export type FastifyPluginCallbackZod< Options extends FastifyPluginOptions = Record, Server extends RawServerBase = RawServerDefault, > = FastifyPluginCallback; /** * FastifyPluginAsyncZod with Zod automatic type inference * * @example * ```typescript * import { FastifyPluginAsyncZod } fromg "fastify-type-provider-zod" * * const plugin: FastifyPluginAsyncZod = async (fastify, options) => { * } * ``` */ export type FastifyPluginAsyncZod< Options extends FastifyPluginOptions = Record, Server extends RawServerBase = RawServerDefault, > = FastifyPluginAsync;