GraphQL如何处理解析器返回值?为何顶层返回字符串仍符合Schema要求
问题场景
我最近开始学习GraphQL,遇到了一个无法解释的异常情况。
以下是复现问题的完整示例代码:
var {ApolloServer, gql} = require("apollo-server") const typeDefs = gql` type Query { test: type1! } type type1 { value: String! } `; const resolvers = { Query: { test: (parent) => { console.log(parent) return "How does my query still work?!" } }, type1: { value: (parent) => { console.log(parent) return "hello!" } } } const server = new ApolloServer({ typeDefs, resolvers }) server.listen(8080).then(({url}) => console.log(`server started on ${url}`))
按照GraphQL的常规解析逻辑,服务器会从外到内逐字段调用匹配的解析器,直到所有请求的字段完成解析。
这个示例最让人困惑的点是:Schema定义里Query.test字段的类型是非空自定义对象type1!,但这个字段的顶层解析器直接返回了一个字符串。按照认知,这种明显的类型不匹配应该直接抛出解析错误,但实际运行完全没有报错。
实际执行的测试查询:
query { test { value } }
最终得到的返回结果完全正常:
{ "data": { "test": { "value": "hello!" } } }
核心疑问:GraphQL服务器对解析器返回值到底做了哪些处理?为什么顶层返回的字符串能被正常处理成要求的对象结构?
核心原理解释
首先要纠正一个常见的认知偏差:
GraphQL的解析阶段本身不会对解析器的返回值做任何类型校验,只会把当前字段的返回值当作"父值(parent)",传递给下一层的子字段解析器作为入参。
对应这个示例的完整执行流程是:
- 最外层执行
Query.test的解析器,拿到返回值字符串"How does my query still work?!",把这个字符串作为type1类型所有子字段的父值暂存 - 解析到
test下的value字段时,服务器找到匹配的type1.value解析器,把上一步拿到的字符串作为parent参数传入,执行解析器拿到硬编码的返回值"hello!" - 整个解析链路走完后,服务器拿到的最终响应结构是包含
value字段的对象,完全匹配type1的Schema定义
整个过程里,服务器既没有校验顶层解析器返回的字符串是不是符合type1的类型要求,也没有尝试把字符串转换成对象——那个字符串只是作为入参传给了type1.value解析器,而这个解析器根本没有读取父值上的任何属性,直接返回了写死的字符串,所以整个流程毫无阻碍就跑通了。
类型校验的触发时机
GraphQL的类型校验不会在解析过程中逐节点触发,只会在两个固定节点启动:
- 最终响应的序列化阶段:所有字段解析完成后,服务器会遍历整个响应树,逐节点校验结构是否符合Schema定义,比如非空字段不能为null、标量类型是否合法
- 自定义标量、枚举类型的独立处理阶段:这类类型有自己的解析、序列化校验逻辑,不符合规范时会直接抛错
回到这个示例,因为走完整个解析链路后,最终的响应结构完全匹配Schema要求,序列化校验阶段自然不会抛出任何错误。
你可以做个简单验证复现报错: - 删掉
resolvers里的type1.value解析器,让服务器走默认字段解析逻辑 - 再执行刚才的查询,这时候就会直接报错:
Cannot return null for non-nullable field type1.value
报错原因是:没有自定义解析器时,服务器会默认从父值(也就是顶层解析器返回的字符串)上寻找和字段同名的value属性,字符串本身不存在这个属性,自然取到null,触发非空字段的校验逻辑。
内容的提问来源于stack exchange,提问作者Carl
相关产品推荐
相关产品推荐

