Skip to content

Commit 18530c0

Browse files
authored
Support zod@4 (#153)
1 parent 63bc5a3 commit 18530c0

20 files changed

+831
-215
lines changed

.github/dependabot.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,4 @@ updates:
1212
interval: 'weekly'
1313
open-pull-requests-limit: 10
1414
ignore:
15-
- dependency-name: '@typescript-eslint/eslint-plugin'
16-
- dependency-name: '@typescript-eslint/parser'
1715
- dependency-name: '@types/node'

README.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type ZodSerializerCompilerOptions = {
4848
```js
4949
import Fastify from 'fastify';
5050
import { createSerializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
51-
import z from 'zod';
51+
import { z } from 'zod/v4';
5252

5353
const app = Fastify();
5454

@@ -77,7 +77,7 @@ app.listen({ port: 4949 });
7777
import fastify from 'fastify';
7878
import fastifySwagger from '@fastify/swagger';
7979
import fastifySwaggerUI from '@fastify/swagger-ui';
80-
import { z } from 'zod';
80+
import { z } from 'zod/v4';
8181

8282
import {
8383
jsonSchemaTransform,
@@ -182,16 +182,16 @@ fastifyApp.setErrorHandler((err, req, reply) => {
182182

183183
## How to create refs to the schemas?
184184

185-
It is possible to create refs to the schemas by using the `createJsonSchemaTransformObject` function. You provide the schemas as an object and fastifySwagger will create a OpenAPI document in which the schemas are referenced. The following example creates a ref to the `User` schema and will include the `User` schema in the OpenAPI document.
185+
When provided, this package will automatically create refs using the `jsonSchemaTransformObject` function. You register the schemas to the global zod registry and give it an `id` and fastifySwagger will create a OpenAPI document in which the schemas are referenced. The following example creates a ref to the `User` schema and will include the `User` schema in the OpenAPI document.
186186

187187
```ts
188188
import fastifySwagger from '@fastify/swagger';
189189
import fastifySwaggerUI from '@fastify/swagger-ui';
190190
import fastify from 'fastify';
191-
import { z } from 'zod';
191+
import { z } from 'zod/v4';
192192
import type { ZodTypeProvider } from 'fastify-type-provider-zod';
193193
import {
194-
createJsonSchemaTransformObject,
194+
jsonSchemaTransformObject,
195195
jsonSchemaTransform,
196196
serializerCompiler,
197197
validatorCompiler,
@@ -202,6 +202,8 @@ const USER_SCHEMA = z.object({
202202
name: z.string().describe('The name of the user'),
203203
});
204204

205+
z.globalRegistry.add(USER_SCHEMA, { id: 'User' })
206+
205207
const app = fastify();
206208
app.setValidatorCompiler(validatorCompiler);
207209
app.setSerializerCompiler(serializerCompiler);
@@ -216,11 +218,7 @@ app.register(fastifySwagger, {
216218
servers: [],
217219
},
218220
transform: jsonSchemaTransform,
219-
transformObject: createJsonSchemaTransformObject({
220-
schemas: {
221-
User: USER_SCHEMA,
222-
},
223-
}),
221+
transformObject: jsonSchemaTransformObject,
224222
});
225223

226224
app.register(fastifySwaggerUI, {
@@ -258,7 +256,7 @@ run();
258256
## How to create a plugin?
259257

260258
```ts
261-
import { z } from 'zod';
259+
import { z } from 'zod/v4';
262260
import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
263261

264262
const plugin: FastifyPluginAsyncZod = async function (fastify, _opts) {

package.json

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,28 @@
22
"name": "fastify-type-provider-zod",
33
"version": "4.0.2",
44
"description": "Zod Type Provider for Fastify@5",
5-
"main": "dist/index.js",
6-
"types": "dist/index.d.ts",
5+
"type": "module",
6+
"main": "./dist/cjs/index.js",
7+
"module": "./dist/esm/index.js",
8+
"exports": {
9+
"require": "./dist/cjs/index.js",
10+
"import": "./dist/esm/index.js"
11+
},
12+
"types": "./dist/cjs/index.d.ts",
713
"files": ["README.md", "LICENSE", "dist"],
814
"scripts": {
9-
"build": "tsc",
15+
"build": "tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json",
1016
"test": "npm run build && npm run typescript && vitest",
1117
"test:coverage": "vitest --coverage",
12-
"lint": "biome check . && tsc --project tsconfig.lint.json --noEmit",
18+
"lint": "biome check .",
1319
"lint:fix": "biome check --write .",
1420
"typescript": "tsd",
21+
"prepare": "npm run build",
1522
"prepublishOnly": "npm run build"
1623
},
1724
"peerDependencies": {
1825
"fastify": "^5.0.0",
19-
"zod": "^3.14.2"
26+
"zod": "^3.25.7"
2027
},
2128
"repository": {
2229
"url": "https://github.com/turkerdev/fastify-type-provider-zod"
@@ -29,24 +36,22 @@
2936
},
3037
"homepage": "https://github.com/turkerdev/fastify-type-provider-zod",
3138
"dependencies": {
32-
"@fastify/error": "^4.0.0",
33-
"zod-to-json-schema": "^3.23.3"
39+
"@fastify/error": "^4.1.0"
3440
},
3541
"devDependencies": {
36-
"@biomejs/biome": "^1.9.3",
37-
"@fastify/swagger": "^9.1.0",
38-
"@fastify/swagger-ui": "^5.0.1",
42+
"@biomejs/biome": "^1.9.4",
43+
"@fastify/swagger": "^9.5.1",
44+
"@fastify/swagger-ui": "^5.2.2",
3945
"@kibertoad/biome-config": "^1.2.1",
4046
"@types/node": "^20.16.10",
41-
"@vitest/coverage-v8": "^2.1.2",
47+
"@vitest/coverage-v8": "^3.1.4",
4248
"fastify": "^5.0.0",
4349
"fastify-plugin": "^5.0.1",
4450
"oas-validator": "^5.0.8",
45-
"openapi-types": "^12.1.3",
46-
"tsd": "^0.31.2",
47-
"typescript": "^5.6.2",
48-
"vitest": "^2.1.2",
49-
"zod": "^3.23.8"
51+
"tsd": "^0.32.0",
52+
"typescript": "^5.8.3",
53+
"vitest": "^3.1.4",
54+
"zod": "^3.25.36"
5055
},
5156
"tsd": {
5257
"directory": "types"

src/core.ts

Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SwaggerTransform, SwaggerTransformObject } from '@fastify/swagger'
12
import type {
23
FastifyPluginAsync,
34
FastifyPluginCallback,
@@ -9,14 +10,11 @@ import type {
910
RawServerDefault,
1011
} from 'fastify'
1112
import type { FastifySerializerCompiler } from 'fastify/types/schema'
12-
import type { OpenAPIV2, OpenAPIV3, OpenAPIV3_1 } from 'openapi-types'
13-
import type { z } from 'zod'
13+
import { z } from 'zod/v4'
1414

1515
import { InvalidSchemaError, ResponseSerializationError, createValidationError } from './errors'
16-
import { resolveRefs } from './ref'
17-
import { convertZodToJsonSchema } from './zod-to-json'
16+
import { zodRegistryToJson, zodSchemaToJson } from './zod-to-json'
1817

19-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2018
type FreeformRecord = Record<string, any>
2119

2220
const defaultSkipList = [
@@ -38,8 +36,16 @@ interface Schema extends FastifySchema {
3836
hide?: boolean
3937
}
4038

41-
export const createJsonSchemaTransform = ({ skipList }: { skipList: readonly string[] }) => {
42-
return ({ schema, url }: { schema: Schema; url: string }) => {
39+
type CreateJsonSchemaTransformOptions = {
40+
skipList?: readonly string[]
41+
schemaRegistry?: z.core.$ZodRegistry<{ id?: string | undefined }>
42+
}
43+
44+
export const createJsonSchemaTransform = ({
45+
skipList = defaultSkipList,
46+
schemaRegistry = z.globalRegistry,
47+
}: CreateJsonSchemaTransformOptions): SwaggerTransform<Schema> => {
48+
return ({ schema, url }) => {
4349
if (!schema) {
4450
return {
4551
schema,
@@ -61,20 +67,17 @@ export const createJsonSchemaTransform = ({ skipList }: { skipList: readonly str
6167
for (const prop in zodSchemas) {
6268
const zodSchema = zodSchemas[prop]
6369
if (zodSchema) {
64-
transformed[prop] = convertZodToJsonSchema(zodSchema)
70+
transformed[prop] = zodSchemaToJson(zodSchema, schemaRegistry, 'input')
6571
}
6672
}
6773

6874
if (response) {
6975
transformed.response = {}
7076

71-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7277
for (const prop in response as any) {
73-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
74-
const schema = resolveSchema((response as any)[prop])
78+
const zodSchema = resolveSchema((response as any)[prop])
7579

76-
const transformedResponse = convertZodToJsonSchema(schema)
77-
transformed.response[prop] = transformedResponse
80+
transformed.response[prop] = zodSchemaToJson(zodSchema, schemaRegistry, 'output')
7881
}
7982
}
8083

@@ -89,25 +92,48 @@ export const createJsonSchemaTransform = ({ skipList }: { skipList: readonly str
8992
}
9093
}
9194

92-
export const jsonSchemaTransform = createJsonSchemaTransform({
93-
skipList: defaultSkipList,
94-
})
95+
export const jsonSchemaTransform = createJsonSchemaTransform({})
96+
97+
type CreateJsonSchemaTransformObjectOptions = {
98+
schemaRegistry?: z.core.$ZodRegistry<{ id?: string | undefined }>
99+
}
95100

96101
export const createJsonSchemaTransformObject =
97-
({ schemas }: { schemas: Record<string, z.ZodTypeAny> }) =>
98-
(
99-
input:
100-
| { swaggerObject: Partial<OpenAPIV2.Document> }
101-
| { openapiObject: Partial<OpenAPIV3.Document | OpenAPIV3_1.Document> },
102-
) => {
102+
({
103+
schemaRegistry = z.globalRegistry,
104+
}: CreateJsonSchemaTransformObjectOptions): SwaggerTransformObject =>
105+
(input) => {
103106
if ('swaggerObject' in input) {
104107
console.warn('This package currently does not support component references for Swagger 2.0')
105108
return input.swaggerObject
106109
}
107110

108-
return resolveRefs(input.openapiObject, schemas)
111+
const inputSchemas = zodRegistryToJson(schemaRegistry, 'input')
112+
const outputSchemas = zodRegistryToJson(schemaRegistry, 'output')
113+
114+
for (const key in outputSchemas) {
115+
if (inputSchemas[key]) {
116+
throw new Error(
117+
`Collision detected for schema "${key}". The is already an input schema with the same name.`,
118+
)
119+
}
120+
}
121+
122+
return {
123+
...input.openapiObject,
124+
components: {
125+
...input.openapiObject.components,
126+
schemas: {
127+
...input.openapiObject.components?.schemas,
128+
...inputSchemas,
129+
...outputSchemas,
130+
},
131+
},
132+
} as ReturnType<SwaggerTransformObject>
109133
}
110134

135+
export const jsonSchemaTransformObject = createJsonSchemaTransformObject({})
136+
111137
export const validatorCompiler: FastifySchemaCompiler<z.ZodTypeAny> =
112138
({ schema }) =>
113139
(data) => {
@@ -129,7 +155,6 @@ function resolveSchema(maybeSchema: z.ZodTypeAny | { properties: z.ZodTypeAny })
129155
throw new InvalidSchemaError(JSON.stringify(maybeSchema))
130156
}
131157

132-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
133158
type ReplacerFunction = (this: any, key: string, value: any) => any
134159

135160
export type ZodSerializerCompilerOptions = {

src/errors.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import createError from '@fastify/error'
22
import type { FastifyError } from 'fastify'
3-
import type { ZodError, ZodIssue, ZodIssueCode } from 'zod'
3+
import type { z } from 'zod/v4'
44

5-
export class ResponseSerializationError extends createError<[{ cause: ZodError }]>(
5+
export class ResponseSerializationError extends createError<[{ cause: z.ZodError }]>(
66
'FST_ERR_RESPONSE_SERIALIZATION',
77
"Response doesn't match the schema",
88
500,
99
) {
10-
cause!: ZodError
10+
cause!: z.ZodError
1111

1212
constructor(
1313
public method: string,
1414
public url: string,
15-
options: { cause: ZodError },
15+
options: { cause: z.ZodError },
1616
) {
1717
super({ cause: options.cause })
18+
19+
this.cause = options.cause
1820
}
1921
}
2022

@@ -32,11 +34,11 @@ const ZodFastifySchemaValidationErrorSymbol = Symbol.for('ZodFastifySchemaValida
3234

3335
export type ZodFastifySchemaValidationError = {
3436
[ZodFastifySchemaValidationErrorSymbol]: true
35-
keyword: ZodIssueCode
37+
keyword: string
3638
instancePath: string
3739
schemaPath: string
3840
params: {
39-
issue: ZodIssue
41+
issue: z.ZodIssue
4042
}
4143
message: string
4244
}
@@ -59,10 +61,10 @@ export const hasZodFastifySchemaValidationErrors = (
5961
error.validation.length > 0 &&
6062
isZodFastifySchemaValidationError(error.validation[0])
6163

62-
export const createValidationError = (error: ZodError): ZodFastifySchemaValidationError[] =>
63-
error.errors.map((issue) => ({
64+
export const createValidationError = (error: z.ZodError): ZodFastifySchemaValidationError[] =>
65+
error.issues.map((issue) => ({
6466
[ZodFastifySchemaValidationErrorSymbol]: true,
65-
keyword: issue.code,
67+
keyword: issue.code ?? 'custom',
6668
instancePath: `/${issue.path.join('/')}`,
6769
schemaPath: `#/${issue.path.join('/')}/${issue.code}`,
6870
params: {

index.ts renamed to src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@ export {
55
type ZodSerializerCompilerOptions,
66
jsonSchemaTransform,
77
createJsonSchemaTransform,
8+
jsonSchemaTransformObject,
89
createJsonSchemaTransformObject,
910
serializerCompiler,
1011
validatorCompiler,
1112
createSerializerCompiler,
12-
} from './src/core'
13+
} from './core'
1314

1415
export {
1516
type ZodFastifySchemaValidationError,
1617
ResponseSerializationError,
1718
InvalidSchemaError,
1819
hasZodFastifySchemaValidationErrors,
1920
isResponseSerializationError,
20-
} from './src/errors'
21+
} from './errors'

0 commit comments

Comments
 (0)