- 后端
- GraphQL
- API设计
【免费下载链接】type-graphql
Create GraphQL schema and resolvers with TypeScript, using classes and decorators!
导读:本文系统讲解 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-validator2. 用装饰器声明校验规则
在@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 }),编译阶段仍然存在两个需要留意的点:
- 依赖问题:由于源码中 src/resolvers/validate-arg.ts 对
class-validator的类型引用了@ts-ignore注释规避编译错误,但tsc在解析类型时仍可能报error TS2307: Cannot find module 'class-validator'。因此建议将class-validator安装为 dev dependency,以保证tsc无错编译。 - 彻底移除依赖的替代方案:如果希望完全从
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!
相关推荐
TypeGraphQL 参数与输入校验实战指南:class-validator 自动验证与自定义 validateFn 完整解析
TypeGraphQL 参数与输入校验实战指南:class validator 自动验证与自定义 validateFn 完整解析 导读 GraphQL API
后端GraphQLAPI设计TypeGraphQL 参数与输入校验(Validation)完整指南:class-validator 集成与自定义校验器
TypeGraphQL 参数与输入校验(Validation)完整指南:class validator 集成与自定义校验器 TypeGraphQL 内置了参数(
后端GraphQLAPI设计TypeGraphQL 参数与输入校验完整指南:基于 class-validator 与自定义验证器的实践
TypeGraphQL 参数与输入校验完整指南:基于 class validator 与自定义验证器的实践 本指南系统讲解 TypeGraphQL 内置的参数与
后端GraphQLAPI设计
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考