{"id":13455097,"url":"https://github.com/elierotenberg/fastify-zod","last_synced_at":"2025-04-09T05:09:50.618Z","repository":{"id":40657022,"uuid":"421415656","full_name":"elierotenberg/fastify-zod","owner":"elierotenberg","description":"Zod integration with Fastify","archived":false,"fork":false,"pushed_at":"2024-02-27T11:07:56.000Z","size":1062,"stargazers_count":215,"open_issues_count":18,"forks_count":23,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-29T22:32:30.104Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/elierotenberg.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-10-26T12:28:43.000Z","updated_at":"2025-02-18T16:50:54.000Z","dependencies_parsed_at":"2023-02-06T22:01:41.259Z","dependency_job_id":"b560b8ef-390f-4912-bf1b-0d6fb8217a86","html_url":"https://github.com/elierotenberg/fastify-zod","commit_stats":{"total_commits":28,"total_committers":3,"mean_commits":9.333333333333334,"dds":0.0714285714285714,"last_synced_commit":"6e996153a48125e907badbfd71a1be665b4ad947"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elierotenberg%2Ffastify-zod","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elierotenberg%2Ffastify-zod/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elierotenberg%2Ffastify-zod/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/elierotenberg%2Ffastify-zod/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/elierotenberg","download_url":"https://codeload.github.com/elierotenberg/fastify-zod/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247980837,"owners_count":21027808,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T08:01:01.265Z","updated_at":"2025-04-09T05:09:50.602Z","avatar_url":"https://github.com/elierotenberg.png","language":"TypeScript","funding_links":[],"categories":["APIs and Servers","TypeScript"],"sub_categories":[],"readme":"# fastify-zod\n\n## Why?\n\n`fastify` is awesome and arguably the best Node http server around.\n\n`zod` is awesome and arguably the best TypeScript modeling / validation library around.\n\nUnfortunately, `fastify` and `zod` don't work together very well. [`fastify` suggests using `@sinclair/typebox`](https://www.fastify.io/docs/latest/TypeScript/#typebox), which is nice but is nowhere close to `zod`. This library allows you to use `zod` as your primary source of truth for models with nice integration with `fastify`, `fastify-swagger` and OpenAPI `typescript-fetch` generator.\n\n## Features\n\n- Define your models using `zod` in a single place, without redundancy / conflicting sources of truth\n- Use your models in business logic code and get out of the box type-safety in `fastify`\n- First-class support for `fastify-swagger` and `openapitools-generator/typescript-fetch`\n- Referential transparency, including for `enum`\n- Deduplication of structurally equivalent models\n- Internal generated JSON Schemas available for reuse\n\n## Setup\n\n- Install `fastify-zod`\n\n```\nnpm i fastify-zod\n```\n\n- Define your models using `zod`\n\n```ts\nconst TodoItemId = z.object({\n  id: z.string().uuid(),\n});\n\nenum TodoStateEnum {\n  Todo = `todo`,\n  InProgress = `in progress`,\n  Done = `done`,\n}\n\nconst TodoState = z.nativeEnum(TodoStateEnum);\n\nconst TodoItem = TodoItemId.extend({\n  label: z.string(),\n  dueDate: z.date().optional(),\n  state: TodoState,\n});\n\nconst TodoItems = z.object({\n  todoItems: z.array(TodoItem),\n});\n\nconst TodoItemsGroupedByStatus = z.object({\n  todo: z.array(TodoItem),\n  inProgress: z.array(TodoItem),\n  done: z.array(TodoItem),\n});\n\nconst models = {\n  TodoItemId,\n  TodoItem,\n  TodoItems,\n  TodoItemsGroupedByStatus,\n};\n```\n\n- Register `fastify` types\n\n```ts\nimport type { FastifyZod } from \"fastify-zod\";\n\n// Global augmentation, as suggested by\n// https://www.fastify.io/docs/latest/Reference/TypeScript/#creating-a-typescript-fastify-plugin\ndeclare module \"fastify\" {\n  interface FastifyInstance {\n    readonly zod: FastifyZod\u003ctypeof models\u003e;\n  }\n}\n\n// Local augmentation\n// See below for register()\nconst f = await register(fastify(), { jsonSchemas });\n```\n\n- Register `fastify-zod` with optional config for `fastify-swagger`\n\n```ts\nimport { buildJsonSchemas, register } from \"fastify-zod\";\n\nconst f = fastify();\n\nawait register(f, {\n  jsonSchemas: buildJsonSchemas(models),\n  swaggerOptions: {\n    // See https://github.com/fastify/fastify-swagger\n  },\n  swaggerUiOptions: {\n    // See https://github.com/fastify/fastify-swagger-ui\n  },\n  transformSpec: {}, // optional, see below\n});\n```\n\n- Define fastify routes using simplified syntax and get automatic type inference\n\n```ts\nf.zod.post(\n  `/item`,\n  {\n    operationId: `postTodoItem`,\n    body: `TodoItem`,\n    reply: `TodoItems`,\n  },\n  async ({ body: nextItem }) =\u003e {\n    /* body is correctly inferred as TodoItem */\n    if (state.todoItems.some((prevItem) =\u003e prevItem.id === nextItem.id)) {\n      throw new BadRequest(`item already exists`);\n    }\n    state.todoItems = [...state.todoItems, nextItem];\n    /* reply is typechecked against TodoItems */\n    return state;\n  }\n);\n```\n\n- Generate transformed spec with first-class support for downstream `openapitools-generator`\n\n```ts\nconst transformedSpecJson = await f\n  .inject({\n    method: `get`,\n    url: `/documentation_transformed/json`,\n  })\n  .then((res) =\u003e res.body);\n\nawait writeFile(\n  join(__dirname, `..`, `..`, `openapi.transformed.json`),\n  transformedSpecJson,\n  { encoding: `utf-8` }\n);\n```\n\n- Generate OpenAPI Client with `openapitools-generator`\n\n`openapi-generator-cli generate`\n\n- For multiple response types / status codes, use `response` instead of `reply`:\n\n```ts\nf.zod.get(\n  `/item/:id`,\n  {\n    operationId: `getTodoItem`,\n    params: `TodoItemId`,\n    response: {\n      200: `TodoItem`,\n      404: `TodoItemNotFoundError`,\n    },\n  },\n  async ({ params: { id } }, reply) =\u003e {\n    const item = state.todoItems.find((item) =\u003e item.id === id);\n    if (item) {\n      return item;\n    }\n    reply.code(404);\n    return {\n      id,\n      message: `item not found`,\n    };\n  }\n);\n```\n\n- For custom error messages, you must enable error messages when building the schemas, as well as [configuring fastify to handle them](https://www.fastify.io/docs/latest/Reference/Validation-and-Serialization/#schemaerrorformatter):\n\n```ts\n// Define custom messages\nconst TodoItemId = z.object({\n  id: z.string().uuid(\"this is not a valid id!\"),\n});\n\n// Then configure fastify\nconst f = fastify({\n  ajv: {\n    customOptions: {\n      allErrors: true,\n    },\n  },\n  plugins: [require(\"ajv-errors\")],\n});\n\nawait register(f, {\n  jsonSchemas: buildJsonSchemas(models, { errorMessages: true }),\n});\n```\n\n## API\n\n### `buildJsonSchemas(models: Models, options: BuildJsonSchemasOptions = {}): BuildJonSchemaResult\u003ctypeof models\u003e`\n\nBuild JSON Schemas and `$ref` function from Zod models.\n\nThe result can be used either with `register` (recommended, see [example in tests](./src/__tests__/server.fixtures.ts)) or directly with `fastify.addSchema` using the `$ref` function (legacy, see [example in tests](./src/__tests__/server.legacy.fixtures.ts)).\n\n#### `Models`\n\nRecord mapping model keys to Zod types. Keys will be used to reference models in routes definitions.\n\nExample:\n\n```ts\nconst TodoItem = z.object({\n  /* ... */\n});\nconst TodoList = z.object({\n  todoItems: z.array(TodoItem),\n});\n\nconst models = {\n  TodoItem,\n  TodoList,\n};\n```\n\n#### `BuildJsonSchemasOptions = {}`\n\n##### `BuildJsonSchemasOptions.$id: string = \"Schemas\"`: `$id` of the generated schema (defaults to \"Schemas\")\n\n##### `BuildJsonSchemasOptions.target: `jsonSchema7`|`openApi3` = \"jsonSchema7\"`: _jsonSchema7_ (default) or _openApi3_\n\nGenerates either `jsonSchema7` or `openApi3` schema. See [`zod-to-json-schema`](https://github.com/StefanTerdell/zod-to-json-schema#options-object).\n\n#### `BuildJsonSchemasResult\u003ctypeof models\u003e = { schemas: JsonSchema[], $ref: $ref\u003ctypeof models\u003e }`\n\nThe result of `buildJsonSchemas` has 2 components: an array of schemas that can be added directly to fastify using `fastify.addSchema`, and a `$ref` function that returns a `{ $ref: string }` object that can be used directly.\n\nIf you simply pass the result to `register`, you won't have to care about this however.\n\n```ts\nconst { schemas, $ref } = buildJsonSchemas(models, { $id: \"MySchema\" });\n\nfor (const schema of schemas) {\n  fastify.addSchema(schema);\n}\n\nequals($ref(\"TodoItem\"), {\n  $ref: \"MySchema#/properties/TodoItem\",\n});\n```\n\n### `buildJsonSchema($id: string, Type: ZodType)` (_deprecated_)\n\nShorthand to `buildJsonSchema({ [$id]: Type }).schemas[0]`.\n\n### `register(f: FastifyInstance, { jsonSchemas, swaggerOptions?: = {} }: RegisterOptions`\n\nAdd schemas to `fastify` and decorate instance with `zod` property to add strongly-typed routes (see `fastify.zod` below).\n\n### `RegisterOptions\u003ctypeof models\u003e`\n\n#### `RegisterOptions\u003ctypeof models\u003e.jsonSchema`\n\nThe result of `buildJsonSchemas(models)` (see above).\n\n##### `RegisterOptions\u003ctypeof models\u003e.swaggerOptions = FastifyDynamicSwaggerOptions \u0026 { transformSpec: TransformSpecOptions }`\n\nIf present, this options will automatically register `fastify-swagger` in addition to `fastify.zod`.\n\nAny options will be passed directly to `fastify-swagger` so you may refer to [their documentation](https://github.com/fastify/fastify-swagger).\n\nIn addition to `fastify-swagger` options, you can pass an additional property, `transformSpec`, to expose a transformed version of the original spec (see below).\n\n```ts\nawait register(f, {\n  jsonSchemas: buildJsonSchemas(models),\n  swaggerOptions: {\n    swagger: {\n      info: {\n        title: `Fastify Zod Test Server`,\n        description: `Test Server for Fastify Zod`,\n        version: `0.0.0`,\n      },\n    },\n  },\n  swaggerUiOptions: {\n    routePrefix: `/swagger`,\n    staticCSP: true,\n  },\n  transformSpec: {\n    /* see below */\n  },\n});\n```\n\n##### `TransformSpecOptions = { cache: boolean = false, routePrefix?: string, options?: TransformOptions }`\n\nIf this property is present on the `swaggerOptions`, then in addition to routes added to `fastify` by `fastify-swagger`, a transformed version of the spec is also exposed. The transformed version is semantically equivalent but benefits from several improvements, notably first-class support for `openapitools-generator-cli` (see below).\n\n`cache` caches the transformed spec. As `SpecTransformer` can be computationally expensive, this may be useful if used in production. Defaults to `false`.\n\n`routePrefix` is the route used to expose the transformed spec, similar to the `routePrefix` option of `fastify-swagger`. Defaults to `${swaggerOptions.routePrefix}_transformed`. Since `swaggerOptions.routePrefix` defaults to `/documentation`, then the default if no `routePrefix` is provided in either options is `/documentation_transformed`.\nThe exposed routes are `/${routePrefix}/json` and `/${routePrefix}/yaml` for JSON and YAML respectively versions of the transformed spec.\n\n`options` are options passed to `SpecTransformer.transform` (see below). By default all transforms are applied.\n\n## `fastify.zod.(delete|get|head|options|patch|post|put)(url: string, config: RouteConfig, handler)`\n\nAdd route with strong typing.\n\nExample:\n\n```ts\nf.zod.put(\n  \"/:id\",\n  {\n    operationId: \"putTodoItem\",\n    params: \"TodoItemId\", // this is a key of \"models\" object above\n    body: \"TodoItem\",\n    reply: {\n      description: \"The updated todo item\",\n      key: \"TodoItem\",\n    },\n  },\n  async ({ params: { id }, body: item }) =\u003e {\n    /* ... */\n  }\n);\n```\n\n### withRefResolver: (options: FastifyDynamicSwaggerOptions) =\u003e FastifyDynamicSwaggerOptions\n\nWraps `fastify-swagger` options providing a sensible default [`refResolver` function](https://github.com/fastify/fastify-swagger#managing-your-refs) compatible with using the `$ref` function returned by buildJsonSchemas`.\n\n`register` automatically uses this under the hood so this is only required if you are using the result of `buildJsonSchemas` directly without using `register`.\n\n### SpecTransformer(spec: ApiSpec)\n\n`SpecTransformer` takes an API spec (typically the output of `/openapi/json` when using `fastify-swagger`) and applies various transforms. This class is used under the hood by `register` when `swaggerOptions.transformSpec` is set so you probably don't need to use it directly.\n\nThe transforms should typically be semantically transparent (no semantic difference) but applies some spec-level optimization and most importantly works around the many quirks of the `typescript-fetch` generator of `openapitools-generator-cli`.\n\n`SpecTransformer` is a stateful object that mutates itself internally, but the original spec object is not modified.\n\nAvailable transforms:\n\n- `rewriteSchemasAbsoluteRefs` transform\n\nTransforms `$ref`s relative to a schema to refs relative to the global spec.\n\nExample input:\n\n```json\n{\n  \"components\": {\n    \"schemas\": {\n      \"Schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"Item\": {\n            /* ... */\n          },\n          \"Items\": {\n            \"type\": \"array\",\n            \"items\": {\n              // \"#\" refers to \"Schema\" scope\n              \"$ref\": \"#/properties/Item\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nOutput:\n\n```json\n{\n  \"components\": {\n    \"schemas\": {\n      \"Schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"Item\": {\n            /* ... */\n          },\n          \"Items\": {\n            \"type\": \"array\",\n            \"items\": {\n              // \"#\" refers to global scope\n              \"$ref\": \"#/components/schemas/Schema/properties/Item\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\n- `extractSchemasProperties` transform\n\nExtract `properties` of schemas into new schemas and rewrite all `$ref`s to point to the new schema.\n\nExample input:\n\n```json\n{\n  \"components\": {\n    \"schemas\": {\n      \"Schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"Item\": {\n            /* ... */\n          },\n          \"Items\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Schema/properties/Item\"\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nOutput:\n\n```json\n{\n  \"components\": {\n    \"schemas\": {\n      \"Schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"Item\": {\n            \"$ref\": \"#/components/schemas/Schema_TodoItem\"\n          },\n          \"Items\": {\n            \"$ref\": \"#/components/schemas/Schema_TodoItems\"\n          }\n        }\n      },\n      \"Schema_TodoItem\": {\n        /* ... */\n      },\n      \"Schema_TodoItems\": {\n        \"type\": \"array\",\n        \"items\": {\n          \"$ref\": \"#/components/schemas/Schema_TodoItem\"\n        }\n      }\n    }\n  }\n}\n```\n\n- `mergeRefs` transform\n\nFinds deeply nested structures equivalent to existing schemas and replace them with `$ref`s to this schema. In practice this means deduplication and more importantly, referential equivalence in addition to structrural equivalence. This is especially useful for `enum`s since in TypeScript to equivalent enums are not assignable to each other.\n\nExample input:\n\n```json\n{\n  \"components\": {\n    \"schemas\": {\n      \"TodoItemState\": {\n        \"type\": \"string\",\n        \"enum\": [\"todo\", \"in progress\", \"done\"]\n      },\n      \"TodoItem\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"state\": {\n            \"type\": \"string\",\n            \"enum\": [\"todo\", \"in progress\", \"done\"]\n          }\n        }\n      }\n    }\n  }\n}\n{\n  \"mergeRefs\": [{\n    \"$ref\": \"TodoItemState#\"\n  }]\n}\n```\n\nOutput:\n\n```json\n{\n  \"components\": {\n    \"schemas\": {\n      \"TodoItemState\": {\n        \"type\": \"string\",\n        \"enum\": [\"todo\", \"in progress\", \"done\"]\n      },\n      \"TodoItem\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"state\": {\n            \"$ref\": \"#/components/schemas/TodoItemState\"\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nIn the typical case, you will not create each ref explicitly, but rather use the `$ref` function provided by `buildJsonSchemas`:\n\n```ts\n{\n  mergeRefs: [$ref(\"TodoItemState\")];\n}\n```\n\n- `deleteUnusedSchemas` transform\n\nDelete all schemas that are not referenced anywhere, including in `paths`. This is useful to remove leftovers of the previous transforms.\n\nExample input:\n\n```json\n{\n  \"components\": {\n    \"schemas\": {\n      // Schema_TodoItem has been extracted,\n      // there are no references to this anymore\n      \"Schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"TodoItem\": {\n            \"$ref\": \"#/components/schemas/Schema_TodoItem\"\n          }\n        }\n      },\n      \"Schema_TodoItem\": {\n        /* ... */\n      }\n    }\n  },\n  \"paths\": {\n    \"/item\": {\n      \"get\": {\n        \"responses\": {\n          \"200\": {\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  // This used to be #/components/Schema/properties/TodoItem\n                  // but has been transformed by extractSchemasProperties\n                  \"$ref\": \"#/components/schemas/Schema_TodoItem\"\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n```\n\nOutput:\n\n```json\n{\n  \"components\": {\n    \"schemas\": {\n      // \"Schema\" has been deleted\n      \"Schema_TodoItem\": {\n        /* ... */\n      }\n    }\n  },\n  \"paths\": {\n    /* ... */\n  }\n}\n```\n\n- `schemaKeys` option\n\nThis option controls the behavior of newly created schemas (e.g. during `extractSchemasProperties` transform).\n\nAvailable configurations:\n\n- `schemaKeys.removeInitialSchemasPrefix`: remove `schemaKey` prefix of initial schemas to create less verbose schema names, e.g. `TodoState` instead of `MySchema_TodoState`\n\n- `schemaKeys.changeCase`: change case of generated schema keys. Defaults to `preserve`. In this case, original schema key and property key prefixes are preserved, and segments are underscore-separated.\n\nIn case of schema key conflict, an error will be thrown during `transform`.\n\n#### SpecTransformer#transform(options: TransformOptions)\n\nApplies the given transforms.\n\nDefault options:\n\n```ts\n{\n  rewriteAbsoluteRefs?: boolean = true,\n  extractSchemasProperties?: boolean = true,\n  mergeRefs?: { $ref: string }[] = [],\n  deleteUnusedSchemas?: boolean = true,\n  schemaKeys?: {\n    removeInitialSchemasPrefix: boolean = false,\n    changeCase: \"preserve\" | \"camelCase\" | \"PascalCase\" | \"snake_case\" | \"param-case\" = \"preserve\"\n  } = {}\n}\n```\n\nAll transforms default to `true` except `mergeRefs` that you must explicitly configure.\n\n#### SpecTransformer#getSpec(): Spec\n\nReturn the current state of the spec. This is typically called after `transform` to use the transformed spec.\n\n## Usage with `openapitools`\n\nTogether with `fastify-swagger`, and `SpecTransformer` this library supports downstream client code generation using `openapitools-generator-cli`.\n\nRecommended use is with `register` and `fastify.inject`.\n\nFor this you need to first generate the spec file, then run `openapitools-generator`:\n\n```ts\nconst jsonSchemas = buildJsonSchemas(models);\n\nawait register(f, {\n  jsonSchemas,\n  swaggerOptions: {\n    openapi: {\n      /* ... */\n    },\n    exposeRoute: true,\n    transformSpec: {\n      routePrefix: \"/openapi_transformed\",\n      options: {\n        mergeRefs: [$ref(\"TodoItemState\")],\n      },\n    },\n  },\n});\n\nconst spec = await f\n  .inject({\n    method: \"get\",\n    url: \"/openapi_transformed/json\",\n  })\n  .then((spec) =\u003e spec.json());\n\nwriteFileSync(\"openapi-spec.json\", JSON.stringify(spec), { encoding: \"utf-8\" });\n```\n\n`openapi-generator-cli generate`\n\nWe recommend running this as part as the build step of your app, see [package.json](./package.json).\n\n## Caveats\n\nUnfortunately and despite best efforts by `SpecTransformer`, the OpenAPI generator has many quirks and limited support for some features. Complex nested arrays are sometimes not validated / parsed correctly, discriminated unions have limited support, etc.\n\n## License\n\nMIT License Copyright (c) Elie Rotenberg\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felierotenberg%2Ffastify-zod","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felierotenberg%2Ffastify-zod","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felierotenberg%2Ffastify-zod/lists"}