☰
TypeGraphQL 参数与输入校验完整指南:class-validator 与自定义 validateFn 深度实践
2026/9/28 2:26:01 网站建设 项目流程
  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!

项目地址:https://gitcode.com/gh_mirrors/ty/type-graphql
点击查看免费下载

导读:本文系统讲解 TypeGraphQL 内建的参数(Argument)与输入(Input)自动校验能力。文章以官方文档 docs/validation.md 与 2.0.0-rc.2 版本文档为骨架,结合仓库源码(validate-arg.ts、build-context.ts、ArgumentValidationError.ts)与官方示例(examples/automatic-validation、examples/custom-validation),带你掌握:如何用class-validator装饰器声明输入约束、如何在buildSchema与@Arg()/@Args()两个层级开启或自定义校验、客户端收到校验错误时如何格式化响应,以及如何用自定义validateFn接入 Joi 等第三方校验库。

为什么需要参数与输入校验

GraphQL 的类型系统本身只能保证"字段是否存在"以及"字段类型是否正确"(String、Int、Float、Boolean 等),却无法表达更细粒度的业务规则——例如"邮箱字段必须真的是合法邮箱""密码必须长于 8 个字符""数字必须在 0 到 255 之间"。

最常见的做法是为每种数据类型编写自定义 Scalar,例如GraphQLEmail(来自graphql-custom-types)。但当数据类型五花八门(信用卡号、base64、IP、URL……)时,为每一个场景单独创建 Scalar 会变得相当繁琐。正是基于这一痛点,TypeGraphQL 在框架层内建了参数与输入校验支持:默认深度集成 class-validator,借助装饰器把校验规则直接声明在输入类上;同时开放自定义validateFn钩子,允许接入任何其他校验库或自研校验逻辑。

一、集成 class-validator:三步开启自动校验

1. 安装依赖

首先安装class-validator包:

npm install class-validator

2. 用装饰器声明校验规则

在@InputType()(或参数类)的字段上叠加class-validator提供的校验装饰器。以官方示例 examples/automatic-validation/recipe.input.ts 为例,从"裸字段"升级为"带约束字段":

升级前:

@InputType() export class RecipeInput { @Field() title: string; @Field({ nullable: true }) description?: string; }

升级后:

import { MaxLength, Length } from "class-validator"; @InputType() export class RecipeInput { @Field() @MaxLength(30) title: string; @Field({ nullable: true }) @Length(30, 255) description?: string; }

这样title被限制为最多 30 个字符,description长度必须落在 30 到 255 之间。class-validator提供的远不止@Length、@MaxLength:还包括@IsEmail、@IsInt、@Min/@Max、@Matches、@IsEnum等数十种内置校验装饰器,完整清单可查阅class-validator官方文档的 Validation Decorators 一节。

3. 在 buildSchema 中开启开关

校验功能默认是关闭的,需要在buildSchema选项中显式设置validate: true:

import { buildSchema } from "type-graphql"; const schema = await buildSchema({ resolvers: [RecipeResolver], validate: true, // 开启 'class-validator' 集成 });

开启之后,TypeGraphQL 会在每次 resolver 执行前,根据类上声明的装饰器自动校验注入的参数与输入。在 resolver 方法内部,可以 100% 确定输入已经合法:

@Resolver(of => Recipe) export class RecipeResolver { @Mutation(returns => Recipe) async addRecipe(@Arg("input") recipeInput: RecipeInput): Promise<Recipe> { // 这里可以放心输入已通过校验 console.assert(recipeInput.title.length <= 30); console.assert(recipeInput.description.length >= 30); console.assert(recipeInput.description.length <= 255); } }

官方可运行示例见 examples/automatic-validation/index.ts,其中同时开启了emitSchemaFile用于输出 schema 文件。

二、默认值、优先级与细粒度控制

全局开关默认值

虽然文档中强调"This feature is enabled by default"是早期版本的表述,但从当前仓库源码看,src/schema/build-context.ts 的BuildContext.reset()中this.validate = false,即当前版本默认关闭自动校验,需要显式开启。若确实不需要校验,可显式关闭:

const schema = await buildSchema({ resolvers: [RecipeResolver], validate: false, // 关闭自动校验,或传入默认配置对象 });

按参数单独开启

全局未开启时,仍可在单个参数上单独开启校验:

class RecipeResolver { @Mutation(returns => Recipe) async addRecipe(@Arg("input", { validate: true }) recipeInput: RecipeInput) { // ... } }

传入 ValidatorOptions

validate选项不仅可以是布尔值,还可以是class-validator的ValidatorOptions对象,例如用于校验组(validation groups):

class RecipeResolver { @Mutation(returns => Recipe) async addRecipe( @Arg("input", { validate: { groups: ["admin"] } }) recipeInput: RecipeInput, ) { // ... } }

从源码 src/schema/build-context.ts 可以看到,ValidateSettings类型定义为boolean | ValidatorOptions,也就是说在buildSchema里同样可以直接传配置对象,例如validate: { groups: ["admin"] }。

校验参数的优先级与合并

在 src/resolvers/validate-arg.ts 中可以看到,参数级设置(argValidateSettings)优先级高于全局设置(globalValidateSettings),并且最终执行时会做对象合并:

const validate = argValidateSettings !== undefined ? argValidateSettings : globalValidateSettings; const validatorOptions: ValidatorOptions = { ...(typeof globalValidateSettings === "object" ? globalValidateSettings : {}), ...(typeof argValidateSettings === "object" ? argValidateSettings : {}), };

这一设计让你可以在全局开启校验的同时,为个别敏感参数追加更严格的配置。

三、与 GraphQL 类型系统的天然分工

TypeGraphQL 对class-validator做了两处贴合 GraphQL 语义的默认调整(src/resolvers/validate-arg.ts):

  • skipMissingProperties默认置为true:因为 GraphQL 运行时本身会独立检查参数/字段是否存在,缺少的必填参数会在 GraphQL 层直接被拒绝;
  • forbidUnknownValues默认置为false:因为 GraphQL 运行时同样会拦截 schema 之外的多余数据。

相应地,由于 GraphQL 已保证字段类型(String、Int、Float、Boolean 等)的正确性,在输入类上完全不需要再叠加@IsOptional、@Allow、@IsString、@IsInt这类与类型/可空性重复的装饰器。

唯一需要特别注意的场景是嵌套输入与数组:嵌套校验不会自动生效,必须显式使用@ValidateNested()装饰器(校验嵌套对象)或{ each: true }选项(校验数组中的每个元素),嵌套校验才能真正工作。

四、校验失败时的响应格式

客户端视角的错误 JSON

当客户端发送不合法数据时:

mutation ValidationMutation { addRecipe( input: { # 太长了! title: "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet" } ) { title creationDate } }

TypeGraphQL 内部会抛出ArgumentValidationError。默认情况下,bootstrap 指南(docs/bootstrap.md)中的apollo-server会将该错误格式化为符合GraphQLFormattedError接口的 JSON,客户端将收到如下响应:

{ "errors": [ { "message": "Argument Validation Error", "locations": [ { "line": 2, "column": 3 } ], "path": ["addRecipe"], "extensions": { "code": "INTERNAL_SERVER_ERROR", "exception": { "validationErrors": [ { "target": { "title": "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet" }, "value": "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet", "property": "title", "children": [], "constraints": { "maxLength": "title must be shorter than or equal to 30 characters" } } ], "stacktrace": [ "Error: Argument Validation Error", " at Object.<anonymous> (/type-graphql/src/resolvers/validate-arg.ts:29:11)", " at Generator.throw (<anonymous>)", " at rejected (/type-graphql/node_modules/tslib/tslib.js:105:69)", " at processTicksAndRejections (internal/process/next_tick.js:81:5)" ] } } } ], "data": null }

extensions.exception.validationErrors数组中每条记录包含target(被校验对象)、value(实际传入值)、property(出错的字段名)、children(嵌套错误)以及constraints(命中的校验约束及其消息),信息量非常完整,便于前端直接定位问题字段。

底层实现:ArgumentValidationError

在 src/errors/graphql/ArgumentValidationError.ts 中可以看到,ArgumentValidationError继承自 GraphQL 的GraphQLError,并在构造函数中把validationErrors数组挂到extensions上:

export class ArgumentValidationError extends GraphQLError { override readonly extensions!: { code: "BAD_USER_INPUT"; validationErrors: ValidationError[]; [attributeName: string]: unknown; }; constructor(validationErrors: ValidationError[]) { super("Argument Validation Error", { extensions: { code: "BAD_USER_INPUT", validationErrors, }, }); Object.setPrototypeOf(this, new.target.prototype); } }

值得注意的是,extensions.code在错误类上被声明为"BAD_USER_INPUT",而 Apollo 默认格式化输出时仍显示"INTERNAL_SERVER_ERROR"。如果你希望客户端看到更语义化的错误码(如ARGUMENT_VALIDATION_ERROR),可以自定义ApolloServer配置中的formatError函数,将携带ValidationError数组的GraphQLError转换为期望的输出格式。

抛错链路

从 src/resolvers/helpers.ts 可以看到完整的调用链:resolver 执行时,getParams会对每个@Arg()/@Args()参数调用validateArg;而 src/resolvers/validate-arg.ts 中通过动态import("class-validator")引入validateOrReject执行实际校验,失败时统一抛出ArgumentValidationError:

try { if (Array.isArray(argValue)) { await Promise.all( argValue .filter(shouldArgBeValidated) .map(argItem => validateOrReject(argItem, validatorOptions)), ); } else { await validateOrReject(argValue, validatorOptions); } return argValue; } catch (err) { throw new ArgumentValidationError(err as ValidationError[]); }

这段代码同时揭示了两个细节:其一,class-validator通过动态导入按需加载,只有实际启用校验时才引入依赖;其二,数组参数会逐个元素校验({ each: true }在参数数组场景下的等价行为)。另外,shouldArgBeValidated只对非空对象进行校验(src/resolvers/validate-arg.ts),标量值直接放行。

五、自定义校验器 validateFn

全局级 validateFn

不想依赖class-validator时,可以完全使用其他校验库或自研逻辑。做法是在buildSchema中提供一个自定义函数validateFn,它接收三个参数:

  • argValue:@Arg()或@Args()注入的实际值;
  • argType:运行时类型信息(例如String或RecipeInput类);
  • resolverData:resolver 执行上下文,类型为泛型ResolverData<TContext>(包含root、args、context、info)。

该函数可以是异步函数;校验通过时返回void(什么都不返回),校验失败时抛出错误。这一点在封装第三方库时尤其要注意。

以接入 joiful 中这样使用:

const schema = await buildSchema({ // ... validateFn: argValue => { // 调用 joiful 校验 const { error } = joiful.validate(argValue); if (error) { // 校验失败时抛出错误 throw error; } }, });

对应的输入类使用 Joi 装饰器声明规则(examples/custom-validation/recipe.input.ts):

import Joiful from "joiful"; import { Field, InputType } from "type-graphql"; import { type Recipe } from "./recipe.type"; @InputType() export class RecipeInput implements Partial<Recipe> { @Field() // Joi 装饰器 @(Joiful.string().required().max(30)) title!: string; @Field({ nullable: true }) // Joi 装饰器 @(Joiful.string().min(30).max(255)) description?: string; }

参数级 validateFn

validateFn同样支持作为@Arg()或@Args()的装饰器选项,实现"全局 + 局部"的灵活组合:

@Resolver() class SampleResolver { @Query() sampleQuery( @Arg("sampleArg", { validateFn: (argValue, argType) => { // 在这里对参数值与类型做自定义处理... }, }) sampleArg: string, ): string { // ... } }

从源码 src/resolvers/validate-arg.ts 可以看到,参数级validateFn(argValidateFn)优先于全局validateFn(globalValidateFn)被调用;一旦配置了validateFn,框架将完全走自定义校验路径,不再触发class-validator逻辑。

一个重要的行为差异

注意:使用自定义校验器时,抛出的错误不会被包装成ArgumentValidationError,而是原样向上传递(src/resolvers/validate-arg.ts 中直接await validateFn(...)后即返回)。因此错误消息、extensions结构以及客户端看到的响应格式,完全取决于你自定义函数的实现,需要自行保证输出的一致性。

六、不启用校验时的两个实践细节

即便完全不使用校验功能(已传{ validate: false }),编译阶段仍然存在两个需要留意的点:

  1. 依赖问题:由于源码中 src/resolvers/validate-arg.ts 对class-validator的类型引用了@ts-ignore注释规避编译错误,但tsc在解析类型时仍可能报error TS2307: Cannot find module 'class-validator'。因此建议将class-validator安装为 dev dependency,以保证tsc无错编译。
  2. 彻底移除依赖的替代方案:如果希望完全从node_modules中去掉体积较大的class-validator,可以在tsconfig.json中开启"skipLibCheck": true,从而压制上述 TS2307 类型导入错误。

两种方案按需取舍:前者保留随时启用校验的可能,后者更精简。

七、小结:两级开关 × 两条路径

TypeGraphQL 的校验体系可以概括为一张清晰的配置矩阵:

维度全局(buildSchema)局部(@Arg / @Args)
class-validator集成validate: true / false / ValidatorOptions{ validate: true }或{ validate: ValidatorOptions }
自定义校验函数validateFn: (argValue, argType, resolverData) => void{ validateFn: ... }
  • 未提供validateFn时走内建class-validator路径,失败抛出ArgumentValidationError(内含validationErrors数组,可通过formatError定制响应);
  • 提供validateFn时走自定义路径,错误不再包装,格式由你掌控;
  • 局部设置优先于全局设置,对象形式的ValidatorOptions会做浅合并。

结合 examples/automatic-validation 与 examples/custom-validation 两个官方示例,你可以直接运行并观察两种路径下的实际错误响应,快速在自己的项目中落地这套参数与输入校验方案。

  • 后端
  • GraphQL
  • API设计

【免费下载链接】type-graphql

Create GraphQL schema and resolvers with TypeScript, using classes and decorators!

项目地址:https://gitcode.com/gh_mirrors/ty/type-graphql
点击查看免费下载

相关推荐

上一篇:探索T2I-Adapter模型的最新进展与未来趋势
下一篇:AWS密钥管理og-aws:10个KMS服务安全最佳实践终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询