先记住这个答案
GraphQL 中 ! 修饰紧随其前的类型。[User] 表示字段返回 null 或 User 数组,数组元素可为 null;[User!] 表示数组可为 null,但每个元素非空;[User]! 表示数组必返回,但元素可为 null;[User!]! 表示数组和元素都不能为 null。若运行时元素为 null 而声明非空,会触发错误并冒泡至最近可空父字段,导致该字段整体为 null。
!放在列表外控制列表本身可空性- 放在列表内控制元素可空性
- 非空元素为 null 会导致整个列表错误
非空标记的解析与错误冒泡
GraphQL 类型修饰符从右向左解析,[User!] 先声明元素非空,再声明列表本身可空;[User]! 则相反。当字段类型包含非空修饰时,若解析结果为 null,执行引擎会向父字段传播错误,并将该字段的返回值视为 null,但是否继续冒泡取决于父字段是否非空。
对于 [User!],若数组返回但某个元素为 null,则整个列表触发错误,且由于列表字段本身可空,该字段会变为 null,错误不会继续向上传播;只有当该字段本身声明为非空(如 [User!]!)时,错误才会继续向上一层传播。例如 friends: [User!] 中一个 null 元素导致 friends 整体为 null,只要 friends 本身可空,错误就不会影响其父对象。
设计好友列表字段的取舍
假设实现社交应用,用户 User 应有 friends 列表。业务要求好友列表始终返回数组,不允许为 null,但某个用户可能没有好友,用空数组表示。同时好友对象必须有 id 和 name,因此声明 friends: [User!]! 最合理。这样数组必存在,元素禁 null,空数组合法。
若误写成 friends: [User],则客户端可能收到 null 或含 null 的数组,迫使客户端防御式检查。若改为 friends: [User!],数组仍可能为 null,需要额外非空判断。通过组合 [User!]!,服务端强制保证返回数组且元素有效,客户端可安全迭代。
空列表与非空标记的边界情况
常见误解是 [User!]! 要求至少有一个元素,但 GraphQL 不提供此语法,空数组 [] 完全合法。若业务要求至少一项,必须自定义标量或使用额外字段如 totalCount,不能仅靠类型系统表达。
当数组内元素来自数据库关联查询时,若数据缺失导致某元素为 null,即使该字段声明为 [User!],整个列表也会变为 null,可能掩盖真实错误。为解决此问题,可在 resolver 中过滤 null 元素,或使用对象字段的非空约束并记录错误,避免整个列表失效。
容易答错的地方
- 认为 `[User!]!` 不允许空数组
- GraphQL 的 Non-Null 只禁止 null,不禁止空集合。
[]是合法返回值,不要依赖类型系统保证非空列表。 - 混淆列表可空与元素可空
[User]!允许元素为 null,但列表必须存在。若业务要求元素也非空,必须写成[User!]!,否则客户端需处理 null 元素。
面试官还会怎么问?
当 `[User!]` 中的某个元素为 null,实际响应中整个字段是 null 还是只该元素为 null?
整个列表字段会变为 null,因为非空元素被破坏导致列表字段错误,且该字段本身可空,所以错误传播后字段整体为 null,同时 errors 数组会记录详情。
如何在 schema 中声明一个必含至少一个元素的数组?
GraphQL 类型系统不提供此能力。可用自定义标量或在 resolver 中校验,若不符合规则则抛出错误,使得字段返回 null 并触发错误。
非空列表嵌套非空,如 `[[User!]]!` 含义是什么?
外层列表非空,内层列表可空但元素非空。即必须返回数组,数组中每个子列表可为 null,但子列表内元素不能为 null。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。