import type { SwaggerTransform, SwaggerTransformObject } from '@fastify/swagger' import type { FastifyPluginAsync, FastifyPluginCallback, FastifyPluginOptions, FastifySchema, FastifySchemaCompiler, FastifySerializerCompiler, FastifyTypeProvider, RawServerBase, RawServerDefault, } from 'fastify' import type { $ZodRegistry, input, output } from 'zod/v4/core' import { $ZodType, globalRegistry, safeParse } from 'zod/v4/core' import { createValidationError, InvalidSchemaError, ResponseSerializationError } from './errors' import { getOASVersion, jsonSchemaToOAS } from './json-to-oas' import { type ZodToJsonConfig, zodRegistryToJson, zodSchemaToJson } from './zod-to-json' type FreeformRecord = Record const defaultSkipList = [ '/documentation/', '/documentation/initOAuth', '/documentation/json', '/documentation/uiConfig', '/documentation/yaml', '/documentation/*', '/documentation/static/*', ] export interface ZodTypeProvider extends FastifyTypeProvider { validator: this['schema'] extends $ZodType ? output : unknown serializer: this['schema'] extends $ZodType ? input : unknown } interface Schema extends FastifySchema { hide?: boolean } type CreateJsonSchemaTransformOptions = { skipList?: readonly string[] schemaRegistry?: $ZodRegistry<{ id?: string | undefined }> zodToJsonConfig?: ZodToJsonConfig } export const createJsonSchemaTransform = ({ skipList = defaultSkipList, schemaRegistry = globalRegistry, zodToJsonConfig = {}, }: CreateJsonSchemaTransformOptions): SwaggerTransform => { return (input) => { if ('swaggerObject' in input) { throw new Error('OpenAPI 2.0 is not supported') } const { schema, url } = input 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 } const oasVersion = getOASVersion(input) for (const prop in zodSchemas) { const zodSchema = zodSchemas[prop] if (zodSchema) { const jsonSchema = zodSchemaToJson( zodSchema, schemaRegistry, 'input', oasVersion, zodToJsonConfig, ) const oasSchema = jsonSchemaToOAS(jsonSchema, oasVersion) transformed[prop] = oasSchema } } if (response) { transformed.response = {} for (const prop in response as any) { const zodSchema = resolveSchema((response as any)[prop]) const jsonSchema = zodSchemaToJson( zodSchema, schemaRegistry, 'output', oasVersion, zodToJsonConfig, ) // Check is the JSON schema is null then return as it is since fastify-swagger will handle it if (jsonSchema.type === 'null') { transformed.response[prop] = jsonSchema continue } const oasSchema = jsonSchemaToOAS(jsonSchema, oasVersion) transformed.response[prop] = oasSchema } } 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: SwaggerTransform = createJsonSchemaTransform({}) type CreateJsonSchemaTransformObjectOptions = { schemaRegistry?: $ZodRegistry<{ id?: string | undefined }> zodToJsonConfig?: ZodToJsonConfig } export const createJsonSchemaTransformObject = ({ schemaRegistry = globalRegistry, zodToJsonConfig = {}, }: CreateJsonSchemaTransformObjectOptions): SwaggerTransformObject => (input) => { if ('swaggerObject' in input) { throw new Error('OpenAPI 2.0 is not supported') } const oasVersion = getOASVersion(input) const inputSchemas = zodRegistryToJson(schemaRegistry, 'input', zodToJsonConfig) const outputSchemas = zodRegistryToJson(schemaRegistry, 'output', zodToJsonConfig) for (const key in outputSchemas) { if (inputSchemas[key]) { throw new Error( `Collision detected for schema "${key}". The is already an input schema with the same name.`, ) } } const jsonSchemas = { ...inputSchemas, ...outputSchemas, } const oasSchemas = Object.fromEntries( Object.entries(jsonSchemas).map(([key, value]) => [key, jsonSchemaToOAS(value, oasVersion)]), ) return { ...input.openapiObject, components: { ...input.openapiObject.components, schemas: { ...input.openapiObject.components?.schemas, ...oasSchemas, }, }, } as ReturnType } export const jsonSchemaTransformObject: SwaggerTransformObject = createJsonSchemaTransformObject({}) export const validatorCompiler: FastifySchemaCompiler<$ZodType> = ({ schema }) => (data) => { const result = safeParse(schema, data) if (result.error) { return { error: createValidationError(result.error) as unknown as Error } } return { value: result.data } } function resolveSchema(maybeSchema: $ZodType | { properties: $ZodType }): $ZodType { if (maybeSchema instanceof $ZodType) { return maybeSchema } if ('properties' in maybeSchema && maybeSchema.properties instanceof $ZodType) { return maybeSchema.properties } throw new InvalidSchemaError(JSON.stringify(maybeSchema)) } type ReplacerFunction = (this: any, key: string, value: any) => any export type ZodSerializerCompilerOptions = { replacer?: ReplacerFunction } export const createSerializerCompiler = ( options?: ZodSerializerCompilerOptions, ): FastifySerializerCompiler<$ZodType | { properties: $ZodType }> => ({ schema: maybeSchema, method, url }) => (data) => { const schema = resolveSchema(maybeSchema) const result = safeParse(schema, data) if (result.error) { throw new ResponseSerializationError(method, url, { cause: result.error, }) } return JSON.stringify(result.data, options?.replacer) } export const serializerCompiler: ReturnType = createSerializerCompiler({}) /** * FastifyPluginCallbackZod with Zod automatic type inference * * @example * ```typescript * import { FastifyPluginCallbackZod } from "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 } from "fastify-type-provider-zod" * * const plugin: FastifyPluginAsyncZod = async (fastify, options) => { * } * ``` */ export type FastifyPluginAsyncZod< Options extends FastifyPluginOptions = Record, Server extends RawServerBase = RawServerDefault, > = FastifyPluginAsync