Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Default values from schema not applied to Request object #13

Open
jstorm31 opened this issue Jul 31, 2023 · 8 comments
Open

Default values from schema not applied to Request object #13

jstorm31 opened this issue Jul 31, 2023 · 8 comments

Comments

@jstorm31
Copy link

I have a validation schema for the limit parameter, which has a default value:

const limitParam = z.number().min(0).optional().default(10)

The middleware validates this parameter in the query, but it does not add default values to the parameters. Therefore, req.query still contains the original incoming object.

router.get(validateRequest(routeSchema.request), (req, res) => {
   console.log(req.query.limit) // -> undefined
})

This may be intentional, as the library is not intended for sanitization and default values.

@AngaBlue
Copy link

AngaBlue commented Aug 11, 2023

I believe this is the desired behaviour for validateRequest* calls but not for processRequest* calls. validateRequest* does not modify the query, params and body whereas processRequest* does.

@jstorm31
Copy link
Author

Oh, I overlooked the processRequest* functions. Thank you! 🙌

@AngaBlue
Copy link

I believe the types still don't function correctly though. It appears that the types are using the schema input types rather than the schema output types.

@RobMayer
Copy link

RobMayer commented Aug 18, 2023

Can confirm
this is broken

with member default set

app.get(
        "/endpoint",
        processRequest({
            query: z.object({
                sort: z.enum(["NAME_ASC", "NAME_DSC"]).default("NAME_ASC"),
            }),
        }),
        async (request, response) => {
            const sort = request.query.sort; // "NAME_ASC" | "NAME_DSC" | undefined
        }
    );

without member default set

    app.get(
        "/endpoint",
        processRequest({
            query: z.object({
                sort: z.enum(["NAME_ASC", "NAME_DSC"]),
            }),
        }),
        async (request, response) => {
            const sort = request.query.sort; // "NAME_ASC" | "NAME_DSC"
        }
    );

with a default on the wrapping object. now the entire object could be undefined...

    app.get(
        "/endpoint",
        processRequest({
            query: z.object({
                sort: z.enum(["NAME_ASC", "NAME_DSC"]).default("NAME_ASC"),
            }).default({}),
        }),
        async (request, response) => {
            request.query // {...} | undefined
            request.query.sort // error as request.query may be undefined
            
        }
    );

even if the default is explicitly stated... still could be undefined.

    app.get(
        "/endpoint",
        processRequest({
            query: z.object({
                sort: z.enum(["NAME_ASC", "NAME_DSC"]).default("NAME_ASC"),
            }).default({
                sort: "NAME_ASC"
            }),
        }),
        async (request, response) => {
            request.query // {...} | undefined
            request.query.sort // error as request.query may be undefined
            
        }
    );

@jstorm31 jstorm31 reopened this Aug 20, 2023
@jstorm31
Copy link
Author

I tested this again today, and processRequest seems to work fine for me with version 1.4.0

// GET /test
router.get('/test', processRequest(
query: z.object({
            limit: z.number().min(0).optional().default(10),
        })
)),
(req, res) => {
   req.query.limit // 10
}

In the library, if safeParse call is successful, they mutate the req.query object here

@AngaBlue
Copy link

AngaBlue commented Aug 21, 2023 via email

@jstorm31
Copy link
Author

Oh I see, you're right, the type is still number | undefined. ☝️

@AngaBlue
Copy link

AngaBlue commented Jan 7, 2024

I ended up writing my own middleware which addresses this issue. The usage is not the same, but regardless you may find it useful. I would submit a PR to fix this issue but it appears as though this project is unmaintained.

import { RequestHandler } from 'express';
import { ZodError, z } from 'zod';

const types = ['query', 'params', 'body'] as const;

/**
 * A middleware generator that validates incoming requests against a set of schemas.
 * @param schemas The schemas to validate against.
 * @returns A middleware function that validates the request.
 */
export default function validate<TParams extends Validation = {}, TQuery extends Validation = {}, TBody extends Validation = {}>(
    schemas: ValidationSchemas<TParams, TQuery, TBody>
): RequestHandler<ZodOutput<TParams>, any, ZodOutput<TBody>, ZodOutput<TQuery>> {
    // Create validation objects for each type
    const validation = {
        params: z.object(schemas.params ?? {}).strict() as z.ZodObject<TParams>,
        query: z.object(schemas.query ?? {}).strict() as z.ZodObject<TQuery>,
        body: z.object(schemas.body ?? {}).strict() as z.ZodObject<TBody>
    };

    return (req, res, next) => {
        const errors: Array<ErrorListItem> = [];

        // Validate all types (params, query, body)
        for (const type of types) {
            const parsed = validation[type].safeParse(req[type]);
            // @ts-expect-error This is fine
            if (parsed.success) req[type] = parsed.data;
            else errors.push({ type, errors: parsed.error });
        }

        // Return all errors if there are any
        if (errors.length > 0) return res.status(400).send(errors.map(error => ({ type: error.type, errors: error.errors })));

        return next();
    };
}

/**
 * The types of validation that can be performed.
 */
type DataType = (typeof types)[number];

/**
 * An error item for a specific type.
 */
interface ErrorListItem {
    type: DataType;
    errors: ZodError<any>;
}

/**
 * Generic validation type for a route (either params, query, or body).
 */
type Validation = Record<string, z.ZodTypeAny>;

/**
 * The schemas provided to the validate middleware.
 */
interface ValidationSchemas<TParams extends Validation, TQuery extends Validation, TBody extends Validation> {
    params?: TParams;
    query?: TQuery;
    body?: TBody;
}

/**
 * The output type of a validation schema.
 */
type ZodOutput<T extends Validation> = z.ZodObject<T>['_output'];

A really basic usage example looks like this:

app.post('/test', validate({ body: { limit: z.number().min(0).optional().default(10) } }), async (req, res) => {
    // req.body.limit -> number
});

I may end up making my own package for this tomorrow given I've used this in a couple projects now. Hope it helps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants