先记住这个答案
Enum在GraphQL中表示schema内静态定义的一组命名值,相当于提供取值白名单的字符串标量。它适合表达不会随业务运行而增长的分类,如任务状态、产品类别;此时schema自带校验,防止非法输入。但若取值全集由数据库记录或用户输入决定,比如文章标签,使用Enum会迫使我们不断修改schema并重发客户端,破坏它原本的静态契约价值。改为String并在resolver中校验合法值,或使用自定义scalar,都能保留灵活性而少打断客户端。
- Enum适合封闭且稳定的取值集
- 动态扩展的集合应改用字符串
- 添加枚举值可能引发客户端兼容问题
Enum的类型机制与校验时机
GraphQL Enum是一种命名类型,所有可能值在schema中通过enum关键字和值列表显式给出。客户端传入参数时,如果值不在列表中,请求会在校验阶段直接失败;字段返回时,服务端也不能返回列表外的值,因此Enum本质是既约束输入端也约束输出端的白名单类型。
它不同于自定义scalar的地方在于:Enum值必须由字母、数字、下划线组成,且不能以数字开头,因此表达力受限。每次新增取值都等于发布新schema版本,虽然GraphQL规范称添加值为向后兼容,但许多代码生成工具会根据schema预先产生类型联结,客户端应用可能因未能解码新值而出错,这构成实际兼容性成本。
订单状态用Enum,用户分类不要用
某电商下单流程有固定五个状态,且状态机流转已经稳定两年,任何新订单都必须从这五个状态中取值。此时用enum OrderStatus { CREATED PAID SHIPPED DELIVERED CANCELED }可以让客户端在编译期就排除非法状态,服务端也不用写取值检查,因为GraphQL引擎已保证。
同一个系统,每个商品允许运营人员创建自定义标签,标签存于关系表并会不定期增加。如果把这个标签字段定义为Enum,每次有人新建标签都要先发布schema,而且已经打开的长连接客户端若未更新schema,用旧schema发tag:NEW_LABEL会收到校验错误。改为tag: String!后在resolver里查询标签表判断存在性,业务规则与schema分离,标签新增无需动schema。
枚举的使用边界与代价
若取值集合的稳定性无法达到“年”级别,Enum会从资产变成负债。动态集合使用Enum的典型失败路径是:后端加值后,旧版客户端在传给新参数时被网关拦截,因为网关按当前schema校验;或者客户端库生成的enum类型不包含新值,导致反序列化抛异常――这些都不是服务端改一行能解决的。
不使用时,必须自己提供替代约束。将字段类型改为String只缓解扩展问题,所有合法性校验转移到resolver或独立校验层,容易遗漏,错误信息也不统一。更折中的做法是定义自定义scalar如Tag并实现parseValue和serialize,由scalar内部维护动态字典,但这样会失去lint工具对Enum值的静态检查。所以,核心取舍是:确定性强弱和扩展频率决定是否启用Enum。
容易答错的地方
- 新增Enum值不算破坏性,所以可以放心扩展
- 规范里新增枚举值确实向后兼容,但生成枚举类型的客户端库常常因未识别新值而抛出反序列化错误,而且许多网关会对未知枚举值直接拒收。实际部署要考虑所有客户端是否能热更新,否则会触发线上故障。
- 用String替代Enum后完全不需要服务端校验
- String类型只保证是字符串,任意值都会通过类型检查。若不额外实现查询常量或字典校验,非法值会直通业务逻辑,最终可能在数据层报错或产生脏数据。校验必须前置到传入时,错误信息才能准确。
面试官还会怎么问?
Enum和自定义scalar在接收限制上有何差异?
Enum的值在schema中静态固定,GraphQL引擎直接校验,错误信息包含允许值列表;自定义scalar需要自己在parseValue中实现判断,且错误只能是标量类型级别的通用信息。前者表达了一种封闭集合,后者本质上仍是解析函数,可接收任意字符串,但你可以实现同样的白名单校验。
如果现在用Enum,将来要加值如何平滑过渡?
理论上加值兼容,但建议分三步:先加值并发布,等待所有客户端更新到支持新值的版本,再逐渐清理旧分支。更稳妥的局面是一开始就不用Enum,把取值放到数据字典或常量配置中,让客户端通过查询读取允许值。
什么时候应该坚持使用Enum,即使扩展频繁?
当扩展每次都同时修改客户端代码且能同步上线时,Enum并没有额外负担。比如内部后台多分支同时改,或者只有一两个调用方。此时可以依靠代码生成把变动同步到各仓库,Enum的静态校验收益更大。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。