fastify / help

Need help with Fastify? File an Issue here.
https://www.fastify.io/
65 stars 8 forks source link

Update request body schema according to the provided route param in runtime #1067

Open amitbeck opened 1 day ago

amitbeck commented 1 day ago

💬 Question here

Is it possible to update a route's request body schema (request.routeOptions.schema.body) according to the provided route param in runtime, using the preValidation hook? It's possible to achieve this using the preHandler hook but that's past the builtin validation stage of the request, and requires manually validating the request body.

This is my schema:

import { type RouteShorthandOptions } from 'fastify';
import { z } from 'zod';

const userInteractionKindSchema = z.enum(['bookmark', 'reaction', 'feedback']);

const bookmarkPayloadSchema = z.object({
  bookmark: z.boolean(),
});
const reactionPayloadSchema = z.object({
  reaction: z.enum(['like', 'dislike']).nullable(),
});
const feedbackPayloadSchema = z.object({
  feedback: z.string(),
});

const userInteractionPayloadSchemaByKind = {
  [userInteractionKindSchema.enum.bookmark]: bookmarkPayloadSchema,
  [userInteractionKindSchema.enum.reaction]: reactionPayloadSchema,
  [userInteractionKindSchema.enum.feedback]: feedbackPayloadSchema,
};

const updateUserInteractionRouteSchema = {
  params: z.object({
    interactionKind: userInteractionKindSchema,
  }),
  body: z.union([
    bookmarkPayloadSchema,
    reactionPayloadSchema,
    feedbackPayloadSchema,
  ]),
} satisfies RouteShorthandOptions['schema'];

And this is my route:

import { type ZodTypeProvider } from 'fastify-type-provider-zod';

fastify.withTypeProvider<ZodTypeProvider>().put(
  '/userInteractions/:interactionKind',
  {
    schema: updateUserInteractionRouteSchema,
    // doesn't work
    preValidation: async request => {
      const interactionKindPayloadSchema = userInteractionPayloadSchemaByKind[
        request.params.interactionKind
      ];
      request.routeOptions.schema.body = interactionKindPayloadSchema /* requires `as any` to work */;
    },
    // works, but requires manual validation
    preHandler: async request => {
      const interactionKindPayloadSchema =
        userInteractionPayloadSchemaByKind[request.params.interactionKind];
      try {
        interactionKindPayloadSchema.parse(request.body);
      } catch (error) {
        throw new Error(
          'Invalid request body for the provided interaction kind',
          { cause: error },
        );
      }
    },
  },
  (request, reply) => { ... },
);

Updating request.routeOptions.schema.body in the preValidation hook doesn't seem to work - when getting a request for the "bookmark" interaction kind with { "reaction": "like" } as the body, which matches another interaction kind's schema, the handler is reached even though it should fail. My current workaround is to manually validate the request body in the preHandler hook.

Your Environment

metcoder95 commented 21 hours ago

I'm not an expert on zod, but that cannot be achieved directly in the schema definition to do a lookup for the property and use a variant?

At the moment the request is received, the context of the request is sealed and cannot be altered (which affects the schema). request.routeOptions properties are read-only.

This has to be achieved using the fastify.setValidationCompiler. There you can customize how the validation gets set.