Skip to content

[openapi-server] Fastify: optional fastify-type-provider-zod mode (native validator/serializer compilers reusing the generated Zod schemas) #322

Description

@benjamineckstein

Labels: enhancement, openapi-server, fastify, design

Design proposal / discussion. Filing to track the option; not a commitment to implement. The hand-rolled validation path stays the default unless we decide otherwise.

Motivation

The generated Fastify router uses none of Fastify 5's native validation/serialization pipeline. It hand-rolls Zod safeParse per param/body and does reply.type().send(). That hand-rolled path is the root cause of several currently-shipping bugs (#313 header casing, #314 query coercion) and the Body: unknown type erasure on request handlers.

fastify-type-provider-zod is the idiomatic way to support modern Fastify when you already have Zod schemas. It wires:

  • validatorCompiler (request validation from Zod),
  • serializerCompiler (response serialization/validation from Zod),
  • withTypeProvider<ZodTypeProvider>() for full request/response type inference.

Crucially, the generated router already imports the generated Zod schemas (e.g. CreatePetRequestSchema), so the schemas needed to drive this mode are already on hand.

Proposed approach (opt-in mode)

Behind a config flag, emit a Fastify router that:

  1. Registers setValidatorCompiler / setSerializerCompiler from fastify-type-provider-zod once.
  2. Uses app.withTypeProvider<ZodTypeProvider>().
  3. Attaches a route schema: { params, querystring, headers, body, response } per operation, built from the generated Zod schemas, instead of inline safeParse blocks.

Keep the current hand-rolled path as the default so the core router stays dependency-free; this is an additional mode, not a forced migration.

What this would fix (structurally)

Caveats / open questions (must be resolved before this is actionable)

  1. Error-contract change. Current major fastify-type-provider-zod throws a FastifyError / ResponseSerializationError on validation failure rather than returning the existing 422 { error, issues } envelope. Adopting this mode silently changes the error contract unless we also generate a setErrorHandler that reshapes those errors back into the documented envelope. This also interacts with [openapi-server] README correction: the generated router DOES wrap service calls in try/catch; document the error-propagation contract #311 (try/catch contract) and the user-owned setErrorHandler documented in the README.
  2. deepObject / delimited query styles. spaceDelimited / pipeDelimited / deepObject query handling (router.ts:~1102-1160) cannot be reproduced by a plain schema.querystring + ajv. These need a pre-parse step or bespoke handling that survives the move to native validation.
  3. HttpError mapping ([openapi-server] Fastify: async HttpError throws return 500 instead of the mapped status because the 200/201 JSON branches don't await the service call #315). The per-route try/catch that maps HttpError to its status would likely move into a central setErrorHandler; the mapping itself still has to be emitted (and HttpError exposes status, not statusCode).
  4. Dependency / version pinning. openapi-server declares no fastify peer today. This mode adds fastify-type-provider-zod (peer zod >=4.1.5; the repo catalog is on zod ^4.4.3, so Zod 4 is fine). Pin exact compatible majors at build time.

Relationship to existing issues

File refs

  • packages/openapi-server/src/plugins/router.ts:1087-1089 (bare route registration, no schema/config), ~1048-1086 (manual generics, Body: unknown), ~1092-1201 (inline safeParse), ~1102-1160 (deepObject/delimited query handling), ~1372-1375 (Zod schema imports already wired)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions