05 checkSchema 声明式校验

1. checkSchema 是什么

ValidationChain 使用链式写法:

body("name")
  .isString()
  .bail()
  .trim()
  .isLength({ min: 1, max: 20 });

checkSchema() 使用对象集中描述多个字段:

import { checkSchema } from "express-validator";

export const createWishSchema = checkSchema({
  name: {
    in: ["body"],
    isString: {
      errorMessage: "name 必须是字符串",
      bail: true,
    },
    trim: true,
    isLength: {
      options: { min: 1, max: 20 },
      errorMessage: "name 必须是 1 到 20 个字符",
    },
  },
});

返回结果既能作为 Express 中间件数组使用,也能通过 .run(req) 手动执行。

2. 完整创建接口 Schema

import { checkSchema } from "express-validator";

export const createWishSchema = checkSchema({
  name: {
    in: ["body"],
    exists: {
      errorMessage: "必须提供 name",
      bail: true,
    },
    isString: {
      errorMessage: "name 必须是字符串",
      bail: true,
    },
    trim: true,
    isLength: {
      options: {
        min: 1,
        max: 20,
      },
      errorMessage: "name 必须是 1 到 20 个字符",
    },
  },

  content: {
    in: ["body"],
    isString: {
      errorMessage: "content 必须是字符串",
      bail: true,
    },
    trim: true,
    isLength: {
      options: {
        min: 1,
        max: 200,
      },
      errorMessage: "content 必须是 1 到 200 个字符",
    },
  },
});

路由:

router.post(
  "/create",
  express.json(),
  createWishSchema,
  validateRequest,
  createWish,
);

3. in:明确数据位置

in: ["body"]
in: ["query"]
in: ["params"]
in: ["headers"]
in: ["cookies"]

可以指定多个位置:

in: ["body", "query"]

但同名字段来源会变得不清晰。日常 API 应优先指定单一位置。

也可以给整个 Schema 设置默认位置:

checkSchema(schema, ["body"]);

4. validator options 的写法

没有 options:

isString: true

带 options 和消息:

isInt: {
  options: {
    min: 1,
    max: 100,
  },
  errorMessage: "必须是 1 到 100 的整数",
  bail: true,
}

某些 validator 的参数不是单个对象,需要使用数组传参。具体应对照对应 ValidationChain 方法签名,避免把多参数错误地写成一个对象。

5. sanitizer

const listSchema = checkSchema(
  {
    page: {
      default: {
        options: 1,
      },
      isInt: {
        options: { min: 1 },
        bail: true,
      },
      toInt: true,
    },

    keyword: {
      optional: true,
      isString: true,
      trim: true,
      isLength: {
        options: { max: 50 },
      },
    },
  },
  ["query"],
);

和 ValidationChain 一样,sanitizer 的执行顺序会影响后续验证。Schema 适合集中查看配置,但长对象中的执行顺序没有链式代码那么直观,因此复杂转换要格外谨慎。

6. optional

nickname: {
  in: ["body"],
  optional: {
    options: {
      values: "undefined",
    },
  },
  isString: true,
  trim: true,
}

简单形式:

optional: true

PATCH Schema 经常把字段设为 optional,但通常还需要检查“至少提交一个允许更新的字段”,可以结合 checkExact()matchedData() 和自定义请求级规则处理。

7. 自定义 validator

endDate: {
  in: ["body"],
  isISO8601: {
    bail: true,
  },
  custom: {
    custom: (endDate: string, { req }) => {
      const startDate = String(req.body.startDate);

      if (new Date(endDate) < new Date(startDate)) {
        throw new Error("endDate 不能早于 startDate");
      }

      return true;
    },
  },
}

异步函数同样受支持:

email: {
  in: ["body"],
  isEmail: { bail: true },
  custom: {
    custom: async (email: string) => {
      const exists = await User.findOne({ where: { email } });

      if (exists !== null) {
        throw new Error("邮箱已存在");
      }

      return true;
    },
  },
}

8. 自定义 sanitizer

tags: {
  in: ["body"],
  customSanitizer: {
    customSanitizer: (value: unknown) =>
      String(value)
        .split(",")
        .map((item) => item.trim()),
  },
  isArray: {
    options: { max: 10 },
  },
}

9. Wildcard 和 Globstar

const orderSchema = checkSchema({
  items: {
    in: ["body"],
    isArray: {
      options: { min: 1, max: 50 },
    },
  },

  "items.*.productId": {
    in: ["body"],
    isInt: {
      options: { min: 1 },
    },
    toInt: true,
  },

  "items.*.quantity": {
    in: ["body"],
    isInt: {
      options: { min: 1, max: 100 },
    },
    toInt: true,
  },
});

teams.**.name 可匹配任意深度字段,但必须限制树形输入规模。

10. 手动运行

const result = await createWishSchema.run(req);

if (!result.isEmpty()) {
  // 处理错误。
}

checkSchema() 返回 RunnableValidationChains,除整体运行外还可以访问其中的链。常规路由仍建议把它直接作为中间件使用。

11. 与 ValidationChain 的选择

场景 ValidationChain checkSchema
少量简单字段 更直观 配置显得偏长
大型表单 链数组较长 字段规则集中
复杂条件 .if() 更易读 对象嵌套容易变深
复用配置 使用函数工厂 可以组合 Schema 对象
执行顺序 一眼可见 需要更仔细阅读
DTO 自动推导 不支持 同样不支持

不要为了追求“配置化”把所有接口都改成 Schema。链式写法和 Schema 可以在同一项目中并存,但团队最好建立选择约定。

12. Schema 组合风险

可以使用对象展开:

import type { Schema } from "express-validator";

const timestampsSchema = {
  createdAt: {
    in: ["query"],
    isISO8601: true,
  },
} satisfies Schema;

但字段覆盖是普通 JavaScript 对象覆盖。多个 Schema 出现同名 key 时,后面的对象会完全替换前面的字段配置,而不是深度合并。

13. 练习题

  1. 把当前 createWishValidation 改写成 checkSchema,但不要修改路由。
  2. 使用默认位置 ["query"] 编写分页 Schema。
  3. 为订单 items 数组编写带 wildcard 的 Schema。
  4. 编写 PATCH 用户资料 Schema,所有字段可选,但出现时必须合法。
  5. 在 Schema 中添加异步邮箱查重,并使用 bail 避免无效查询。
  6. 比较相同接口的 ValidationChain 和 checkSchema 版本,记录哪一种更容易看出执行顺序。

官方参考