@bram-dc/fastify-type-provider-zod 5.0.0 → 5.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +53 -46
  2. package/dist/cjs/core.cjs +137 -0
  3. package/dist/cjs/core.cjs.map +1 -0
  4. package/dist/cjs/core.d.cts +68 -0
  5. package/dist/cjs/errors.cjs +57 -0
  6. package/dist/cjs/errors.cjs.map +1 -0
  7. package/dist/cjs/errors.d.cts +30 -0
  8. package/dist/cjs/index.cjs +16 -0
  9. package/dist/cjs/index.cjs.map +1 -0
  10. package/dist/cjs/index.d.cts +2 -0
  11. package/dist/cjs/json-to-oas.cjs +85 -0
  12. package/dist/cjs/json-to-oas.cjs.map +1 -0
  13. package/dist/cjs/json-to-oas.d.cts +9 -0
  14. package/dist/cjs/zod-to-json.cjs +98 -0
  15. package/dist/cjs/zod-to-json.cjs.map +1 -0
  16. package/dist/cjs/zod-to-json.d.cts +8 -0
  17. package/dist/esm/core.d.ts +68 -0
  18. package/dist/esm/core.js +137 -0
  19. package/dist/esm/core.js.map +1 -0
  20. package/dist/esm/errors.d.ts +30 -0
  21. package/dist/esm/errors.js +57 -0
  22. package/dist/esm/errors.js.map +1 -0
  23. package/dist/esm/index.d.ts +2 -0
  24. package/dist/esm/index.js +16 -0
  25. package/dist/esm/index.js.map +1 -0
  26. package/dist/esm/json-to-oas.d.ts +9 -0
  27. package/dist/esm/json-to-oas.js +85 -0
  28. package/dist/esm/json-to-oas.js.map +1 -0
  29. package/dist/esm/zod-to-json.d.ts +8 -0
  30. package/dist/esm/zod-to-json.js +98 -0
  31. package/dist/esm/zod-to-json.js.map +1 -0
  32. package/package.json +73 -58
  33. package/src/core.ts +244 -0
  34. package/src/errors.ts +99 -0
  35. package/src/index.ts +21 -0
  36. package/src/json-to-oas.ts +109 -0
  37. package/src/zod-to-json.ts +150 -0
  38. package/dist/index.d.ts +0 -2
  39. package/dist/index.js +0 -16
  40. package/dist/src/core.d.ts +0 -58
  41. package/dist/src/core.js +0 -114
  42. package/dist/src/errors.d.ts +0 -35
  43. package/dist/src/errors.js +0 -43
@@ -0,0 +1,98 @@
1
+ import { $ZodRegistry, toJSONSchema, $ZodType } from "zod/v4/core";
2
+ const getSchemaId = (id, io) => {
3
+ return io === "input" ? `${id}Input` : id;
4
+ };
5
+ const getReferenceUri = (id, io) => {
6
+ return `#/components/schemas/${getSchemaId(id, io)}`;
7
+ };
8
+ function isZodDate(entity) {
9
+ return entity instanceof $ZodType && entity._zod.def.type === "date";
10
+ }
11
+ function isZodUnion(entity) {
12
+ return entity instanceof $ZodType && entity._zod.def.type === "union";
13
+ }
14
+ function isZodUndefined(entity) {
15
+ return entity instanceof $ZodType && entity._zod.def.type === "undefined";
16
+ }
17
+ const getOverride = (ctx, io) => {
18
+ if (isZodUnion(ctx.zodSchema)) {
19
+ ctx.jsonSchema.anyOf = ctx.jsonSchema.anyOf?.filter((schema) => Object.keys(schema).length > 0);
20
+ }
21
+ if (isZodDate(ctx.zodSchema)) {
22
+ if (io === "output") {
23
+ ctx.jsonSchema.type = "string";
24
+ ctx.jsonSchema.format = "date-time";
25
+ }
26
+ }
27
+ if (isZodUndefined(ctx.zodSchema)) {
28
+ if (io === "output") {
29
+ ctx.jsonSchema.type = "null";
30
+ }
31
+ }
32
+ };
33
+ const zodSchemaToJson = (zodSchema, registry, io) => {
34
+ const schemaRegistryEntry = registry.get(zodSchema);
35
+ if (schemaRegistryEntry?.id) {
36
+ return {
37
+ $ref: getReferenceUri(schemaRegistryEntry.id, io)
38
+ };
39
+ }
40
+ const tempID = "GEN";
41
+ const tempRegistry = new $ZodRegistry();
42
+ tempRegistry.add(zodSchema, { id: tempID });
43
+ const {
44
+ schemas: { [tempID]: result }
45
+ } = toJSONSchema(tempRegistry, {
46
+ target: "draft-2020-12",
47
+ metadata: registry,
48
+ io,
49
+ unrepresentable: "any",
50
+ cycles: "ref",
51
+ reused: "inline",
52
+ /**
53
+ * The uri option only allows customizing the base path of the `$ref`, and it automatically appends a path to it.
54
+ * As a workaround, we set a placeholder that looks something like this:
55
+ *
56
+ * | marker | always added by zod | meta.id |
57
+ * |__SCHEMA__PLACEHOLDER__| #/$defs/ | User |
58
+ *
59
+ * @example `__SCHEMA__PLACEHOLDER__#/$defs/User"`
60
+ * @example `__SCHEMA__PLACEHOLDER__#/$defs/Group"`
61
+ *
62
+ * @see jsonSchemaReplaceRef
63
+ * @see https://github.com/colinhacks/zod/issues/4750
64
+ */
65
+ uri: () => `__SCHEMA__PLACEHOLDER__`,
66
+ override: (ctx) => getOverride(ctx, io)
67
+ });
68
+ const jsonSchema = { ...result };
69
+ delete jsonSchema.id;
70
+ const jsonSchemaReplaceRef = JSON.stringify(jsonSchema).replaceAll(
71
+ /"__SCHEMA__PLACEHOLDER__#\/\$defs\/(.+?)"/g,
72
+ (_, id) => `"${getReferenceUri(id, io)}"`
73
+ );
74
+ return JSON.parse(jsonSchemaReplaceRef);
75
+ };
76
+ const zodRegistryToJson = (registry, io) => {
77
+ const result = toJSONSchema(registry, {
78
+ target: "draft-2020-12",
79
+ io,
80
+ unrepresentable: "any",
81
+ cycles: "ref",
82
+ reused: "inline",
83
+ uri: (id) => getReferenceUri(id, io),
84
+ override: (ctx) => getOverride(ctx, io)
85
+ }).schemas;
86
+ const jsonSchemas = {};
87
+ for (const id in result) {
88
+ const jsonSchema = { ...result[id] };
89
+ delete jsonSchema.id;
90
+ jsonSchemas[getSchemaId(id, io)] = jsonSchema;
91
+ }
92
+ return jsonSchemas;
93
+ };
94
+ export {
95
+ zodRegistryToJson,
96
+ zodSchemaToJson
97
+ };
98
+ //# sourceMappingURL=zod-to-json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zod-to-json.js","sources":["../../src/zod-to-json.ts"],"sourcesContent":["import type { $ZodDate, $ZodUndefined, $ZodUnion, JSONSchema } from 'zod/v4/core'\nimport { $ZodRegistry, $ZodType, toJSONSchema } from 'zod/v4/core'\n\nconst getSchemaId = (id: string, io: 'input' | 'output') => {\n return io === 'input' ? `${id}Input` : id\n}\n\nconst getReferenceUri = (id: string, io: 'input' | 'output') => {\n return `#/components/schemas/${getSchemaId(id, io)}`\n}\n\nfunction isZodDate(entity: unknown): entity is $ZodDate {\n return entity instanceof $ZodType && entity._zod.def.type === 'date'\n}\n\nfunction isZodUnion(entity: unknown): entity is $ZodUnion {\n return entity instanceof $ZodType && entity._zod.def.type === 'union'\n}\n\nfunction isZodUndefined(entity: unknown): entity is $ZodUndefined {\n return entity instanceof $ZodType && entity._zod.def.type === 'undefined'\n}\n\nconst getOverride = (\n ctx: {\n zodSchema: $ZodType\n jsonSchema: JSONSchema.BaseSchema\n },\n io: 'input' | 'output',\n) => {\n if (isZodUnion(ctx.zodSchema)) {\n // Filter unrepresentable types in unions\n // TODO: Should be fixed upstream and not merged in this plugin.\n // Remove when passed: https://github.com/colinhacks/zod/pull/5013\n ctx.jsonSchema.anyOf = ctx.jsonSchema.anyOf?.filter((schema) => Object.keys(schema).length > 0)\n }\n\n if (isZodDate(ctx.zodSchema)) {\n // Allow dates to be represented as strings in output schemas\n if (io === 'output') {\n ctx.jsonSchema.type = 'string'\n ctx.jsonSchema.format = 'date-time'\n }\n }\n\n if (isZodUndefined(ctx.zodSchema)) {\n // Allow undefined to be represented as null in output schemas\n if (io === 'output') {\n ctx.jsonSchema.type = 'null'\n }\n }\n}\n\nexport const zodSchemaToJson: (\n zodSchema: $ZodType,\n registry: $ZodRegistry<{ id?: string }>,\n io: 'input' | 'output',\n) => JSONSchema.BaseSchema = (zodSchema, registry, io) => {\n const schemaRegistryEntry = registry.get(zodSchema)\n\n /**\n * Checks whether the provided schema is registered in the given registry.\n * If it is present and has an `id`, it can be referenced as component.\n *\n * @see https://github.com/turkerdev/fastify-type-provider-zod/issues/173\n */\n if (schemaRegistryEntry?.id) {\n return {\n $ref: getReferenceUri(schemaRegistryEntry.id, io),\n }\n }\n\n /**\n * Unfortunately, at the time of writing, there is no way to generate a schema with `$ref`\n * using `toJSONSchema` and a zod schema.\n *\n * As a workaround, we create a zod registry containing only the specific schema we want to convert.\n *\n * @see https://github.com/colinhacks/zod/issues/4281\n */\n const tempID = 'GEN'\n const tempRegistry = new $ZodRegistry<{ id?: string }>()\n tempRegistry.add(zodSchema, { id: tempID })\n\n const {\n schemas: { [tempID]: result },\n } = toJSONSchema(tempRegistry, {\n target: 'draft-2020-12',\n metadata: registry,\n io,\n unrepresentable: 'any',\n cycles: 'ref',\n reused: 'inline',\n\n /**\n * The uri option only allows customizing the base path of the `$ref`, and it automatically appends a path to it.\n * As a workaround, we set a placeholder that looks something like this:\n *\n * | marker | always added by zod | meta.id |\n * |__SCHEMA__PLACEHOLDER__| #/$defs/ | User |\n *\n * @example `__SCHEMA__PLACEHOLDER__#/$defs/User\"`\n * @example `__SCHEMA__PLACEHOLDER__#/$defs/Group\"`\n *\n * @see jsonSchemaReplaceRef\n * @see https://github.com/colinhacks/zod/issues/4750\n */\n uri: () => `__SCHEMA__PLACEHOLDER__`,\n override: (ctx) => getOverride(ctx, io),\n })\n\n const jsonSchema = { ...result }\n delete jsonSchema.id\n\n /**\n * Replace the previous generated placeholders with the final `$ref` value\n */\n const jsonSchemaReplaceRef = JSON.stringify(jsonSchema).replaceAll(\n /\"__SCHEMA__PLACEHOLDER__#\\/\\$defs\\/(.+?)\"/g,\n (_, id) => `\"${getReferenceUri(id, io)}\"`,\n )\n\n return JSON.parse(jsonSchemaReplaceRef) as typeof result\n}\n\nexport const zodRegistryToJson: (\n registry: $ZodRegistry<{ id?: string }>,\n io: 'input' | 'output',\n) => Record<string, JSONSchema.BaseSchema> = (registry, io) => {\n const result = toJSONSchema(registry, {\n target: 'draft-2020-12',\n io,\n unrepresentable: 'any',\n cycles: 'ref',\n reused: 'inline',\n uri: (id) => getReferenceUri(id, io),\n override: (ctx) => getOverride(ctx, io),\n }).schemas\n\n const jsonSchemas: Record<string, JSONSchema.BaseSchema> = {}\n for (const id in result) {\n const jsonSchema = { ...result[id] }\n\n delete jsonSchema.id\n\n jsonSchemas[getSchemaId(id, io)] = jsonSchema\n }\n\n return jsonSchemas\n}\n"],"names":[],"mappings":";AAGA,MAAM,cAAc,CAAC,IAAY,OAA2B;AAC1D,SAAO,OAAO,UAAU,GAAG,EAAE,UAAU;AACzC;AAEA,MAAM,kBAAkB,CAAC,IAAY,OAA2B;AAC9D,SAAO,wBAAwB,YAAY,IAAI,EAAE,CAAC;AACpD;AAEA,SAAS,UAAU,QAAqC;AACtD,SAAO,kBAAkB,YAAY,OAAO,KAAK,IAAI,SAAS;AAChE;AAEA,SAAS,WAAW,QAAsC;AACxD,SAAO,kBAAkB,YAAY,OAAO,KAAK,IAAI,SAAS;AAChE;AAEA,SAAS,eAAe,QAA0C;AAChE,SAAO,kBAAkB,YAAY,OAAO,KAAK,IAAI,SAAS;AAChE;AAEA,MAAM,cAAc,CAClB,KAIA,OACG;AACH,MAAI,WAAW,IAAI,SAAS,GAAG;AAI7B,QAAI,WAAW,QAAQ,IAAI,WAAW,OAAO,OAAO,CAAC,WAAW,OAAO,KAAK,MAAM,EAAE,SAAS,CAAC;AAAA,EAChG;AAEA,MAAI,UAAU,IAAI,SAAS,GAAG;AAE5B,QAAI,OAAO,UAAU;AACnB,UAAI,WAAW,OAAO;AACtB,UAAI,WAAW,SAAS;AAAA,IAC1B;AAAA,EACF;AAEA,MAAI,eAAe,IAAI,SAAS,GAAG;AAEjC,QAAI,OAAO,UAAU;AACnB,UAAI,WAAW,OAAO;AAAA,IACxB;AAAA,EACF;AACF;AAEO,MAAM,kBAIgB,CAAC,WAAW,UAAU,OAAO;AACxD,QAAM,sBAAsB,SAAS,IAAI,SAAS;AAQlD,MAAI,qBAAqB,IAAI;AAC3B,WAAO;AAAA,MACL,MAAM,gBAAgB,oBAAoB,IAAI,EAAE;AAAA,IAAA;AAAA,EAEpD;AAUA,QAAM,SAAS;AACf,QAAM,eAAe,IAAI,aAAA;AACzB,eAAa,IAAI,WAAW,EAAE,IAAI,QAAQ;AAE1C,QAAM;AAAA,IACJ,SAAS,EAAE,CAAC,MAAM,GAAG,OAAA;AAAA,EAAO,IAC1B,aAAa,cAAc;AAAA,IAC7B,QAAQ;AAAA,IACR,UAAU;AAAA,IACV;AAAA,IACA,iBAAiB;AAAA,IACjB,QAAQ;AAAA,IACR,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAeR,KAAK,MAAM;AAAA,IACX,UAAU,CAAC,QAAQ,YAAY,KAAK,EAAE;AAAA,EAAA,CACvC;AAED,QAAM,aAAa,EAAE,GAAG,OAAA;AACxB,SAAO,WAAW;AAKlB,QAAM,uBAAuB,KAAK,UAAU,UAAU,EAAE;AAAA,IACtD;AAAA,IACA,CAAC,GAAG,OAAO,IAAI,gBAAgB,IAAI,EAAE,CAAC;AAAA,EAAA;AAGxC,SAAO,KAAK,MAAM,oBAAoB;AACxC;AAEO,MAAM,oBAGgC,CAAC,UAAU,OAAO;AAC7D,QAAM,SAAS,aAAa,UAAU;AAAA,IACpC,QAAQ;AAAA,IACR;AAAA,IACA,iBAAiB;AAAA,IACjB,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,KAAK,CAAC,OAAO,gBAAgB,IAAI,EAAE;AAAA,IACnC,UAAU,CAAC,QAAQ,YAAY,KAAK,EAAE;AAAA,EAAA,CACvC,EAAE;AAEH,QAAM,cAAqD,CAAA;AAC3D,aAAW,MAAM,QAAQ;AACvB,UAAM,aAAa,EAAE,GAAG,OAAO,EAAE,EAAA;AAEjC,WAAO,WAAW;AAElB,gBAAY,YAAY,IAAI,EAAE,CAAC,IAAI;AAAA,EACrC;AAEA,SAAO;AACT;"}
package/package.json CHANGED
@@ -1,61 +1,76 @@
1
1
  {
2
- "name": "@bram-dc/fastify-type-provider-zod",
3
- "version": "5.0.0",
4
- "description": "Zod Type Provider for Fastify@5",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "files": [
8
- "README.md",
9
- "LICENSE",
10
- "dist"
11
- ],
12
- "scripts": {
13
- "build": "tsc",
14
- "test": "npm run build && npm run typescript && vitest",
15
- "test:coverage": "vitest --coverage",
16
- "lint": "biome check . && tsc --project tsconfig.lint.json --noEmit",
17
- "lint:fix": "biome check --write .",
18
- "typescript": "tsd",
19
- "prepublishOnly": "npm run build"
2
+ "name": "@bram-dc/fastify-type-provider-zod",
3
+ "version": "5.0.3",
4
+ "description": "Zod Type Provider for Fastify@5",
5
+ "type": "module",
6
+ "main": "./dist/cjs/index.cjs",
7
+ "module": "./dist/esm/index.js",
8
+ "types": "./dist/cjs/index.d.cts",
9
+ "exports": {
10
+ "require": {
11
+ "types": "./dist/cjs/index.d.cts",
12
+ "default": "./dist/cjs/index.cjs"
20
13
  },
21
- "peerDependencies": {
22
- "fastify": "^5.0.0",
23
- "zod": "^4.0.0-beta.20250412T085909"
24
- },
25
- "repository": {
26
- "url": "https://github.com/turkerdev/fastify-type-provider-zod"
27
- },
28
- "keywords": [
29
- "fastify",
30
- "zod",
31
- "type",
32
- "provider"
33
- ],
34
- "author": "turkerd",
35
- "license": "MIT",
36
- "bugs": {
37
- "url": "https://github.com/turkerdev/fastify-type-provider-zod/issues"
38
- },
39
- "homepage": "https://github.com/turkerdev/fastify-type-provider-zod",
40
- "dependencies": {
41
- "@fastify/error": "^4.0.0"
42
- },
43
- "devDependencies": {
44
- "@biomejs/biome": "^1.9.3",
45
- "@fastify/swagger": "^9.1.0",
46
- "@fastify/swagger-ui": "^5.0.1",
47
- "@kibertoad/biome-config": "^1.2.1",
48
- "@types/node": "^20.16.10",
49
- "@vitest/coverage-v8": "^2.1.2",
50
- "fastify": "^5.0.0",
51
- "fastify-plugin": "^5.0.1",
52
- "oas-validator": "^5.0.8",
53
- "tsd": "^0.31.2",
54
- "typescript": "^5.6.2",
55
- "vitest": "^2.1.2",
56
- "zod": "^4.0.0-beta.20250412T085909"
57
- },
58
- "tsd": {
59
- "directory": "types"
14
+ "default": {
15
+ "types": "./dist/esm/index.d.ts",
16
+ "default": "./dist/esm/index.js"
60
17
  }
61
- }
18
+ },
19
+ "files": [
20
+ "src",
21
+ "dist"
22
+ ],
23
+ "scripts": {
24
+ "build": "vite build",
25
+ "prepare": "npm run build",
26
+ "test:coverage": "vitest --coverage",
27
+ "test:ci": "npm run build && npm run typescript && npm run test:coverage",
28
+ "lint": "biome check . && tsc --noEmit",
29
+ "lint:fix": "biome check --write .",
30
+ "typescript": "tsd",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "repository": {
34
+ "url": "https://github.com/turkerdev/fastify-type-provider-zod"
35
+ },
36
+ "keywords": [
37
+ "fastify",
38
+ "zod",
39
+ "type",
40
+ "provider"
41
+ ],
42
+ "author": "turkerd",
43
+ "license": "MIT",
44
+ "bugs": {
45
+ "url": "https://github.com/turkerdev/fastify-type-provider-zod/issues"
46
+ },
47
+ "homepage": "https://github.com/turkerdev/fastify-type-provider-zod",
48
+ "dependencies": {
49
+ "@fastify/error": "^4.2.0"
50
+ },
51
+ "peerDependencies": {
52
+ "@fastify/swagger": ">=9.5.1",
53
+ "fastify": "^5.0.0",
54
+ "openapi-types": "^12.1.3",
55
+ "zod": ">=3.25.67"
56
+ },
57
+ "devDependencies": {
58
+ "@biomejs/biome": "^2.0.6",
59
+ "@fastify/swagger": "^9.5.1",
60
+ "@fastify/swagger-ui": "^5.2.3",
61
+ "@kibertoad/biome-config": "^2.0.0",
62
+ "@types/node": "^22.16.0",
63
+ "@vitest/coverage-v8": "^3.2.4",
64
+ "fastify": "^5.4.0",
65
+ "fastify-plugin": "^5.0.1",
66
+ "oas-validator": "^5.0.8",
67
+ "tsd": "^0.32.0",
68
+ "typescript": "^5.8.3",
69
+ "unplugin-isolated-decl": "^0.14.5",
70
+ "vitest": "^3.2.4",
71
+ "zod": "^4.0.5"
72
+ },
73
+ "tsd": {
74
+ "directory": "types"
75
+ }
76
+ }
package/src/core.ts ADDED
@@ -0,0 +1,244 @@
1
+ import type { SwaggerTransform, SwaggerTransformObject } from '@fastify/swagger'
2
+ import type {
3
+ FastifyPluginAsync,
4
+ FastifyPluginCallback,
5
+ FastifyPluginOptions,
6
+ FastifySchema,
7
+ FastifySchemaCompiler,
8
+ FastifyTypeProvider,
9
+ RawServerBase,
10
+ RawServerDefault,
11
+ } from 'fastify'
12
+ import type { FastifySerializerCompiler } from 'fastify/types/schema'
13
+ import type { $ZodRegistry, input, output } from 'zod/v4/core'
14
+ import { $ZodType, globalRegistry, safeParse } from 'zod/v4/core'
15
+ import { createValidationError, InvalidSchemaError, ResponseSerializationError } from './errors'
16
+ import { getOASVersion, jsonSchemaToOAS } from './json-to-oas'
17
+ import { zodRegistryToJson, zodSchemaToJson } from './zod-to-json'
18
+
19
+ type FreeformRecord = Record<string, any>
20
+
21
+ const defaultSkipList = [
22
+ '/documentation/',
23
+ '/documentation/initOAuth',
24
+ '/documentation/json',
25
+ '/documentation/uiConfig',
26
+ '/documentation/yaml',
27
+ '/documentation/*',
28
+ '/documentation/static/*',
29
+ ]
30
+
31
+ export interface ZodTypeProvider extends FastifyTypeProvider {
32
+ validator: this['schema'] extends $ZodType ? output<this['schema']> : unknown
33
+ serializer: this['schema'] extends $ZodType ? input<this['schema']> : unknown
34
+ }
35
+
36
+ interface Schema extends FastifySchema {
37
+ hide?: boolean
38
+ }
39
+
40
+ type CreateJsonSchemaTransformOptions = {
41
+ skipList?: readonly string[]
42
+ schemaRegistry?: $ZodRegistry<{ id?: string | undefined }>
43
+ }
44
+
45
+ export const createJsonSchemaTransform = ({
46
+ skipList = defaultSkipList,
47
+ schemaRegistry = globalRegistry,
48
+ }: CreateJsonSchemaTransformOptions): SwaggerTransform<Schema> => {
49
+ return (input) => {
50
+ if ('swaggerObject' in input) {
51
+ throw new Error('OpenAPI 2.0 is not supported')
52
+ }
53
+
54
+ const { schema, url } = input
55
+
56
+ if (!schema) {
57
+ return {
58
+ schema,
59
+ url,
60
+ }
61
+ }
62
+
63
+ const { response, headers, querystring, body, params, hide, ...rest } = schema
64
+
65
+ const transformed: FreeformRecord = {}
66
+
67
+ if (skipList.includes(url) || hide) {
68
+ transformed.hide = true
69
+ return { schema: transformed, url }
70
+ }
71
+
72
+ const zodSchemas: FreeformRecord = { headers, querystring, body, params }
73
+
74
+ const oasVersion = getOASVersion(input)
75
+
76
+ for (const prop in zodSchemas) {
77
+ const zodSchema = zodSchemas[prop]
78
+ if (zodSchema) {
79
+ const jsonSchema = zodSchemaToJson(zodSchema, schemaRegistry, 'input')
80
+ const oasSchema = jsonSchemaToOAS(jsonSchema, oasVersion)
81
+
82
+ transformed[prop] = oasSchema
83
+ }
84
+ }
85
+
86
+ if (response) {
87
+ transformed.response = {}
88
+
89
+ for (const prop in response as any) {
90
+ const zodSchema = resolveSchema((response as any)[prop])
91
+ const jsonSchema = zodSchemaToJson(zodSchema, schemaRegistry, 'output')
92
+
93
+ // Check is the JSON schema is null then return as it is since fastify-swagger will handle it
94
+ if (jsonSchema.type === 'null') {
95
+ transformed.response[prop] = jsonSchema
96
+ continue
97
+ }
98
+
99
+ const oasSchema = jsonSchemaToOAS(jsonSchema, oasVersion)
100
+
101
+ transformed.response[prop] = oasSchema
102
+ }
103
+ }
104
+
105
+ for (const prop in rest) {
106
+ const meta = rest[prop as keyof typeof rest]
107
+ if (meta) {
108
+ transformed[prop] = meta
109
+ }
110
+ }
111
+
112
+ return { schema: transformed, url }
113
+ }
114
+ }
115
+
116
+ export const jsonSchemaTransform: SwaggerTransform<Schema> = createJsonSchemaTransform({})
117
+
118
+ type CreateJsonSchemaTransformObjectOptions = {
119
+ schemaRegistry?: $ZodRegistry<{ id?: string | undefined }>
120
+ }
121
+
122
+ export const createJsonSchemaTransformObject =
123
+ ({
124
+ schemaRegistry = globalRegistry,
125
+ }: CreateJsonSchemaTransformObjectOptions): SwaggerTransformObject =>
126
+ (input) => {
127
+ if ('swaggerObject' in input) {
128
+ throw new Error('OpenAPI 2.0 is not supported')
129
+ }
130
+
131
+ const oasVersion = getOASVersion(input)
132
+
133
+ const inputSchemas = zodRegistryToJson(schemaRegistry, 'input')
134
+ const outputSchemas = zodRegistryToJson(schemaRegistry, 'output')
135
+
136
+ for (const key in outputSchemas) {
137
+ if (inputSchemas[key]) {
138
+ throw new Error(
139
+ `Collision detected for schema "${key}". The is already an input schema with the same name.`,
140
+ )
141
+ }
142
+ }
143
+
144
+ const jsonSchemas = {
145
+ ...inputSchemas,
146
+ ...outputSchemas,
147
+ }
148
+
149
+ const oasSchemas = Object.fromEntries(
150
+ Object.entries(jsonSchemas).map(([key, value]) => [key, jsonSchemaToOAS(value, oasVersion)]),
151
+ )
152
+
153
+ return {
154
+ ...input.openapiObject,
155
+ components: {
156
+ ...input.openapiObject.components,
157
+ schemas: {
158
+ ...input.openapiObject.components?.schemas,
159
+ ...oasSchemas,
160
+ },
161
+ },
162
+ } as ReturnType<SwaggerTransformObject>
163
+ }
164
+
165
+ export const jsonSchemaTransformObject: SwaggerTransformObject = createJsonSchemaTransformObject({})
166
+
167
+ export const validatorCompiler: FastifySchemaCompiler<$ZodType> =
168
+ ({ schema }) =>
169
+ (data) => {
170
+ const result = safeParse(schema, data)
171
+ if (result.error) {
172
+ return { error: createValidationError(result.error) as unknown as Error }
173
+ }
174
+
175
+ return { value: result.data }
176
+ }
177
+
178
+ function resolveSchema(maybeSchema: $ZodType | { properties: $ZodType }): $ZodType {
179
+ if (maybeSchema instanceof $ZodType) {
180
+ return maybeSchema
181
+ }
182
+ if ('properties' in maybeSchema && maybeSchema.properties instanceof $ZodType) {
183
+ return maybeSchema.properties
184
+ }
185
+ throw new InvalidSchemaError(JSON.stringify(maybeSchema))
186
+ }
187
+
188
+ type ReplacerFunction = (this: any, key: string, value: any) => any
189
+
190
+ export type ZodSerializerCompilerOptions = {
191
+ replacer?: ReplacerFunction
192
+ }
193
+
194
+ export const createSerializerCompiler =
195
+ (
196
+ options?: ZodSerializerCompilerOptions,
197
+ ): FastifySerializerCompiler<$ZodType | { properties: $ZodType }> =>
198
+ ({ schema: maybeSchema, method, url }) =>
199
+ (data) => {
200
+ const schema = resolveSchema(maybeSchema)
201
+
202
+ const result = safeParse(schema, data)
203
+ if (result.error) {
204
+ throw new ResponseSerializationError(method, url, { cause: result.error })
205
+ }
206
+
207
+ return JSON.stringify(result.data, options?.replacer)
208
+ }
209
+
210
+ export const serializerCompiler: ReturnType<typeof createSerializerCompiler> =
211
+ createSerializerCompiler({})
212
+
213
+ /**
214
+ * FastifyPluginCallbackZod with Zod automatic type inference
215
+ *
216
+ * @example
217
+ * ```typescript
218
+ * import { FastifyPluginCallbackZod } from "fastify-type-provider-zod"
219
+ *
220
+ * const plugin: FastifyPluginCallbackZod = (fastify, options, done) => {
221
+ * done()
222
+ * }
223
+ * ```
224
+ */
225
+ export type FastifyPluginCallbackZod<
226
+ Options extends FastifyPluginOptions = Record<never, never>,
227
+ Server extends RawServerBase = RawServerDefault,
228
+ > = FastifyPluginCallback<Options, Server, ZodTypeProvider>
229
+
230
+ /**
231
+ * FastifyPluginAsyncZod with Zod automatic type inference
232
+ *
233
+ * @example
234
+ * ```typescript
235
+ * import { FastifyPluginAsyncZod } from "fastify-type-provider-zod"
236
+ *
237
+ * const plugin: FastifyPluginAsyncZod = async (fastify, options) => {
238
+ * }
239
+ * ```
240
+ */
241
+ export type FastifyPluginAsyncZod<
242
+ Options extends FastifyPluginOptions = Record<never, never>,
243
+ Server extends RawServerBase = RawServerDefault,
244
+ > = FastifyPluginAsync<Options, Server, ZodTypeProvider>
package/src/errors.ts ADDED
@@ -0,0 +1,99 @@
1
+ import createError, { type FastifyErrorConstructor } from '@fastify/error'
2
+ import type { FastifyError } from 'fastify'
3
+ import type { FastifySchemaValidationError } from 'fastify/types/schema'
4
+ import type { $ZodError } from 'zod/v4/core'
5
+
6
+ export const InvalidSchemaError: FastifyErrorConstructor<
7
+ {
8
+ code: string
9
+ },
10
+ [string]
11
+ > = createError<[string]>('FST_ERR_INVALID_SCHEMA', 'Invalid schema passed: %s', 500)
12
+
13
+ const ZodFastifySchemaValidationErrorSymbol: symbol = Symbol.for('ZodFastifySchemaValidationError')
14
+
15
+ export type ZodFastifySchemaValidationError = FastifySchemaValidationError & {
16
+ [ZodFastifySchemaValidationErrorSymbol]: true
17
+ }
18
+
19
+ const ResponseSerializationBase: FastifyErrorConstructor<
20
+ {
21
+ code: string
22
+ },
23
+ [
24
+ {
25
+ cause: $ZodError
26
+ },
27
+ ]
28
+ > = createError<[{ cause: $ZodError }]>(
29
+ 'FST_ERR_RESPONSE_SERIALIZATION',
30
+ "Response doesn't match the schema",
31
+ 500,
32
+ )
33
+
34
+ export class ResponseSerializationError extends ResponseSerializationBase {
35
+ cause!: $ZodError
36
+
37
+ constructor(
38
+ public method: string,
39
+ public url: string,
40
+ options: { cause: $ZodError },
41
+ ) {
42
+ super({ cause: options.cause })
43
+
44
+ this.cause = options.cause
45
+ }
46
+ }
47
+
48
+ export function isResponseSerializationError(value: unknown): value is ResponseSerializationError {
49
+ return 'method' in (value as ResponseSerializationError)
50
+ }
51
+
52
+ function isZodFastifySchemaValidationError(
53
+ error: unknown,
54
+ ): error is ZodFastifySchemaValidationError {
55
+ return (
56
+ typeof error === 'object' &&
57
+ error !== null &&
58
+ (error as ZodFastifySchemaValidationError)[ZodFastifySchemaValidationErrorSymbol] === true
59
+ )
60
+ }
61
+
62
+ export function hasZodFastifySchemaValidationErrors(
63
+ error: unknown,
64
+ ): error is Omit<FastifyError, 'validation'> & { validation: ZodFastifySchemaValidationError[] } {
65
+ return (
66
+ typeof error === 'object' &&
67
+ error !== null &&
68
+ 'validation' in error &&
69
+ Array.isArray(error.validation) &&
70
+ error.validation.length > 0 &&
71
+ isZodFastifySchemaValidationError(error.validation[0])
72
+ )
73
+ }
74
+
75
+ function omit<T extends object, K extends keyof T>(obj: T, keys: readonly K[]): Omit<T, K> {
76
+ const result = {} as Omit<T, K>
77
+ for (const key of Object.keys(obj) as Array<keyof T>) {
78
+ if (!keys.includes(key as K)) {
79
+ // @ts-expect-error
80
+ result[key] = obj[key]
81
+ }
82
+ }
83
+ return result
84
+ }
85
+
86
+ export function createValidationError(error: $ZodError): ZodFastifySchemaValidationError[] {
87
+ return error.issues.map((issue) => {
88
+ return {
89
+ [ZodFastifySchemaValidationErrorSymbol]: true,
90
+ keyword: issue.code,
91
+ instancePath: `/${issue.path.join('/')}`,
92
+ schemaPath: `#/${issue.path.join('/')}/${issue.code}`,
93
+ message: issue.message,
94
+ params: {
95
+ ...omit(issue, ['path', 'code', 'message']),
96
+ },
97
+ }
98
+ })
99
+ }
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ export {
2
+ createJsonSchemaTransform,
3
+ createJsonSchemaTransformObject,
4
+ createSerializerCompiler,
5
+ type FastifyPluginAsyncZod,
6
+ type FastifyPluginCallbackZod,
7
+ jsonSchemaTransform,
8
+ jsonSchemaTransformObject,
9
+ serializerCompiler,
10
+ validatorCompiler,
11
+ type ZodSerializerCompilerOptions,
12
+ type ZodTypeProvider,
13
+ } from './core'
14
+
15
+ export {
16
+ hasZodFastifySchemaValidationErrors,
17
+ InvalidSchemaError,
18
+ isResponseSerializationError,
19
+ ResponseSerializationError,
20
+ type ZodFastifySchemaValidationError,
21
+ } from './errors'