You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义Zod验证中间件验证Express查询参数时返回空对象

Express中Zod验证中间件处理Query参数异常问题

我正在开发一个后端学习项目,基于Express框架搭建,自定义了Zod验证中间件。该中间件处理body和params参数时完全正常,但处理query参数时,请求要么挂起无响应,要么返回空对象。

相关代码

验证中间件代码

import type { NextFunction, Request, Response } from "express";
import type { ZodObject, ZodType } from "zod";
import { ZodError } from "zod";

import { HttpError } from "../errors/http-errors";

type Schema = ZodObject | ZodType;
type ParamsRecord = Record<string, string>;
type QueryRecord = Record<string, unknown>;

export interface RequestValidationSchema {
    body?: Schema;
    params?: Schema;
    query?: Schema;
}

const formatedError = (error: ZodError) =>
    error.issues.map((issue) => ({
        path: issue.path.join("."),
        message: issue.message,
    }));

export const validateRequest = (schema: RequestValidationSchema) => {
    return (req: Request, _res: Response, next: NextFunction) => {
        try {
            if (schema.body) {
                const parsedBody = schema.body.parse(req.body) as unknown;
                req.body = parsedBody;
            }

            if (schema.params) {
                const parsedParams = schema.params.parse(
                    req.params,
                ) as ParamsRecord;
                req.params = parsedParams as Request["params"];
            }

            if (schema.query) {
                const parsedQuery = schema.query.parse(
                    req.query,
                ) as QueryRecord;
                req.query = parsedQuery as Request["query"];
            }

            next();
        } catch (error) {
            if (error instanceof ZodError) {
                next(
                    new HttpError(400, "Validation Error", {
                        issue: formatedError(error),
                    }),
                );
                
            } else {
                next(error);
            }
        }
    };
};

路由配置

router.patch(
    "/promotions/:id/toggle-active",
    authenticate,
    canAccess(["admin", "manager"]),
    validateRequest({params: PromotionParamSchema, query:TogglePromotionActiveSchema}),
    togglePromotionActiveHandler
);

测试请求代码

describe("PATCH /promotions/:id/toggle-active - Toggle Promotion Active Status", () => {
    it("should deactivate promotion", async () => {
        const promotion = await createPromotionHelper();

        const response = await request(app)
            .patch(`/sale/promotions/${promotion.data.id}/toggle-active?isActive=false`)
            .set("Cookie", [`accessToken=${adminAccessToken}`])
            .send();

        console.log('res',response.body)
        console.log('res',response.body.message.details)
        expect(response.statusCode).toBe(200);
        expect(response.body.success).toBe(true);

        const verifyResponse = await request(app)
            .get(`/sale/promotions/${promotion.data.id}`)
            .send();
        console.log('ver',verifyResponse.body)
        expect(verifyResponse.body.data.isActive).toBe(false);
    });

控制台输出

res { }

问题原因与修复方案

1. Query参数类型不兼容

Express的req.query类型是ParsedQs(由qs库解析生成),而非普通的Record<string, unknown>。直接将Zod解析后的普通对象赋值给req.query会导致类型冲突,甚至运行时异常。

修复方式:
改用Object.assign合并解析后的参数,避免直接替换:

if (schema.query) {
    const parsedQuery = schema.query.parse(req.query) as QueryRecord;
    Object.assign(req.query, parsedQuery);
}

或者调整类型定义,确保兼容ParsedQs:

import type { ParsedQs } from "qs";

type QueryRecord = ParsedQs & Record<string, unknown>;

// 赋值时
req.query = parsedQuery as ParsedQs;

2. Query参数类型未转换

URL中的Query参数默认都是字符串类型(比如isActive=false实际传递的是字符串"false"),如果Zod Schema定义的是z.boolean(),默认会验证失败,因为字符串不等于布尔值。

修复方式:
使用Zod的coerce方法自动转换类型:

import { z } from "zod";

// 正确的Schema定义
const TogglePromotionActiveSchema = z.object({
  isActive: z.coerce.boolean()
});

3. 全局错误处理缺失

如果验证失败抛出HttpError后,没有对应的全局错误处理中间件返回响应,Express会无法结束请求,导致请求挂起并返回空对象。

修复方式:
添加全局错误处理中间件:

app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
  if (err instanceof HttpError) {
    return res.status(err.statusCode).json({
      success: false,
      message: err.message,
      details: err.details
    });
  }
  // 处理其他未知错误
  res.status(500).json({
    success: false,
    message: "Internal Server Error"
  });
});

4. 调试验证错误

可以在中间件的catch块添加日志,定位具体的验证失败字段:

catch (error) {
  if (error instanceof ZodError) {
    console.log("Zod验证错误详情:", error.issues);
    next(
      new HttpError(400, "Validation Error", {
        issue: formatedError(error),
      }),
    );
  } else {
    console.log("意外错误:", error);
    next(error);
  }
}

内容的提问来源于stack exchange,提问作者BIPIN

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.02 05:13:10