2018年6月版
简介
这是 GraphQL 的规范,GraphQL 是一种查询语言和执行引擎,最初于 2012 年在 Facebook 创建,用于描述客户端-服务器应用程序的数据模型的功能和需求。此开放标准的开发始于 2015 年。
GraphQL 已经发展,并可能在本规范的未来版本中继续发展。GraphQL 规范的先前版本可以在与其 发布标签 匹配的永久链接中找到。最新的工作草案版本可以在 facebook.github.io/graphql/draft/ 中找到。
版权声明
版权所有 © 2015 年至今,Facebook, Inc.
截至 2017 年 9 月 26 日,以下个人或实体已根据开放 Web 基金会最终规范协议 (OWFa 1.0) 提供本规范,该协议可在 openwebfoundation.org 上查阅。
您可以在 github.com/facebook/graphql 上查看本规范的开放 Web 基金会最终规范协议 1.0 版的已签署副本,其中可能还包括上述列表之外的其他方。
您对本规范的使用可能受其他第三方权利的约束。本规范按“原样”提供。贡献者明确声明不承担任何明示、暗示或其他形式的保证,包括与本规范相关的适销性、非侵权性、特定用途适用性或所有权的暗示保证。实施者和用户承担实施或以其他方式使用本规范的全部风险。在任何情况下,任何一方均不对任何其他方因利润损失或任何形式的间接、特殊、附带或后果性损害承担责任,无论其因何种诉讼原因引起,也无论是否基于合同违约、侵权行为(包括疏忽)或其他原因,以及另一方是否已被告知发生此类损害的可能性。
符合性
GraphQL 的符合规范的实现必须满足所有规范性要求。符合性要求在本文件中通过描述性断言和具有明确定义含义的关键词来描述。
本规范的规范性部分中使用的关键词“必须 (MUST)”、“不得 (MUST NOT)”、“必需 (REQUIRED)”、“应该 (SHALL)”、“不应 (SHALL NOT)”、“应当 (SHOULD)”、“不应当 (SHOULD NOT)”、“推荐 (RECOMMENDED)”、“可以 (MAY)”和“可选 (OPTIONAL)”应按照 IETF RFC 2119 中的描述进行解释。这些关键词可能以小写形式出现,但除非明确声明为非规范性,否则仍保留其含义。
GraphQL 的符合规范的实现可以提供额外的功能,但不得在明确禁止或以其他方式导致不符合规范的情况下提供。
符合规范的算法
以祈使语法表达的算法步骤(例如,“返回调用解析器的结果”)应与其包含的算法具有相同的要求级别。在算法步骤中引用的任何算法(例如,“令 completedResult 为调用 CompleteValue() 的结果”)应被解释为具有至少与包含该步骤的算法相同的要求级别。
以算法形式表达的符合性要求可以通过本规范的任何实现方式来满足,只要感知到的结果是等效的即可。本文档中描述的算法旨在易于理解。鼓励实施者包含等效但优化的实现。
有关算法定义和本文档中使用的其他符号约定的更多详细信息,请参见 附录 A。
非规范性部分
本文档的所有内容均为规范性内容,除非明确声明为非规范性部分。
本文档中的示例是非规范性的,旨在帮助理解介绍的概念和规范性部分的行为。示例要么在正文中明确介绍(例如“例如”),要么在示例或反例块中单独列出,如下所示
Example № 1This is an example of a non-normative example.
Counter Example № 2This is an example of a non-normative counter-example.
本文档中的注释是非规范性的,旨在阐明意图,引起对潜在的边缘情况和陷阱的注意,并回答实施过程中出现的常见问题。注释要么在正文中明确介绍(例如“注意:”),要么在注释块中单独列出,如下所示
GraphQL 是一种查询语言,旨在通过提供直观而灵活的语法和系统来构建客户端应用程序,以描述其数据需求和交互。
例如,此 GraphQL 请求将从 Facebook 的 GraphQL 实现中接收 id 为 4 的用户的姓名。
Example № 3{
user(id: 4) {
name
}
}
这将产生以下结果数据 (JSON 格式)
Example № 4{
"user": {
"name": "Mark Zuckerberg"
}
}
GraphQL 不是一种能够进行任意计算的编程语言,而是一种用于查询应用程序服务器的语言,这些服务器的功能在本规范中定义。GraphQL 不强制要求实现它的应用程序服务器使用特定的编程语言或存储系统。相反,应用程序服务器将其功能映射到 GraphQL 编码的统一语言、类型系统和理念。这为产品开发提供了统一的友好界面,并为工具构建提供了强大的平台。
GraphQL 有许多设计原则
由于这些原则,GraphQL 是构建客户端应用程序的强大而高效的环境。针对正在运行的 GraphQL 服务器(在高质量工具的支持下)构建应用程序的产品开发人员和设计师可以快速变得高效,而无需阅读大量文档,也几乎无需或根本无需正式培训。为了实现这种体验,必须有人构建这些服务器和工具。
以下正式规范可作为这些构建者的参考。它描述了语言及其文法、类型系统和用于查询它的内省系统,以及执行和验证引擎及其驱动算法。本规范的目标是为 GraphQL 工具、客户端库和服务器实现的生态系统(跨越组织和平台)提供基础和框架,而这些生态系统尚待构建。我们期待与社区合作以实现这一目标。
客户端使用 GraphQL 查询语言向 GraphQL 服务发出请求。我们将这些请求源称为文档。文档可能包含操作(查询、变更和订阅)以及片段,片段是允许查询重用的常用组合单元。
GraphQL 文档被定义为语法文法,其中终结符是标记(不可分割的词法单元)。这些标记在词法文法中定义,该文法匹配源字符的模式(由双冒号 ::
定义)。
GraphQL 文档表示为 Unicode 字符序列。但是,除了少数例外,GraphQL 的大部分内容仅以原始的非控制 ASCII 范围表示,以便尽可能广泛地兼容尽可能多的现有工具、语言和序列化格式,并避免在文本编辑器和源代码控制中出现显示问题。
非 ASCII Unicode 字符可以自由地出现在 GraphQL 的 StringValue 和 Comment 部分中。
“字节顺序标记”是一种特殊的 Unicode 字符,可能出现在包含 Unicode 的文件的开头,程序可以使用它来确定文本流是 Unicode,文本流的字节序以及要解释的几种 Unicode 编码中的哪一种。
空白字符用于提高源文本的可读性,并充当标记之间的分隔符,并且任何数量的空白字符都可能出现在任何标记之前或之后。标记之间的空白字符对于 GraphQL 文档的语义含义并不重要,但是空白字符可能出现在 String 或 Comment 标记中。
与空白字符一样,行终止符用于提高源文本的可读性,任何数量的行终止符都可能出现在任何其他标记之前或之后,并且对 GraphQL 文档的语义含义没有影响。在任何其他标记中都找不到行终止符。
GraphQL 源文档可能包含单行注释,以 # 标记开头。
注释可以包含除 LineTerminator 之外的任何 Unicode 代码点,因此注释始终由以 # 字符开头的所有代码点组成,直到但不包括行终止符。
注释的行为类似于空白字符,可以出现在任何标记之后或行终止符之前,并且对 GraphQL 文档的语义含义没有影响。
与空白字符和行终止符类似,逗号 (,) 用于提高源文本的可读性并分隔词法标记,但在 GraphQL 文档中,它们在语法和语义上都是无关紧要的。
非重要的逗号字符确保逗号的缺失或存在不会有意义地改变文档的解释语法,因为这可能是其他语言中常见的用户错误。它还允许风格上使用尾随逗号或行终止符作为列表分隔符,这对于源代码的可读性和可维护性通常都是需要的。
GraphQL 文档由几种不可分割的词法标记组成,这些标记在此处的词法文法中通过源 Unicode 字符的模式定义。
标记稍后用作 GraphQL 文档句法文法中的终结符。
在每个词法标记之前和之后,都可能存在任何数量的忽略的标记,包括 WhiteSpace 和 Comment。源文档的任何忽略区域都不重要,但是忽略的源字符可能以重要的方式出现在词法标记中,例如 String 可能包含空白字符。
在解析给定标记时不会忽略任何字符,例如,在定义 FloatValue 的字符之间不允许有空白字符。
! | $ | ( | ) | ... | : | = | @ | [ | ] | { | | | } |
GraphQL 文档包含标点符号以描述结构。GraphQL 是一种数据描述语言,而不是编程语言,因此 GraphQL 缺少通常用于描述数学表达式的标点符号。
GraphQL 文档中充满了命名事物:操作、字段、参数、类型、指令、片段和变量。所有名称都必须遵循相同的语法形式。
GraphQL 中的名称区分大小写。也就是说,name
、Name
和 NAME
都指的是不同的名称。下划线是有意义的,这意味着 other_name
和 othername
是两个不同的名称。
GraphQL 中的名称仅限于此ASCII可能的字符子集,以支持与尽可能多的其他系统进行互操作。
GraphQL 文档描述了 GraphQL 服务或客户端操作的完整文件或请求字符串。文档包含多个定义,可以是可执行的,也可以代表 GraphQL 类型系统。
仅当文档包含 OperationDefinition 并且仅包含 ExecutableDefinition 时,GraphQL 服务才能执行文档。但是,不包含 OperationDefinition 或包含 TypeSystemDefinition 或 TypeSystemExtension 的文档仍然可以被解析和验证,以允许客户端工具表示可能出现在许多单独文件中的许多 GraphQL 用法。
如果文档仅包含一个操作,则该操作可以是未命名的或以简写形式表示,这两种形式都省略了 query 关键字和操作名称。否则,如果 GraphQL 文档包含多个操作,则每个操作都必须命名。当向 GraphQL 服务提交包含多个操作的文档时,还必须提供要执行的所需操作的名称。
仅寻求提供 GraphQL 查询执行的 GraphQL 服务可以选择仅包含 ExecutableDefinition,并从 Definition 中省略 TypeSystemDefinition 和 TypeSystemExtension 规则。
query | mutation | subscription |
GraphQL 建模了三种类型的操作
每个操作都由一个可选的操作名称和一个选择集表示。
例如,此 mutation 操作可能“喜欢”一个故事,然后检索新的喜欢数量
Example № 5mutation {
likeStory(storyID: 12345) {
story {
likeCount
}
}
}
查询简写
如果文档仅包含一个 query 操作,并且该 query 操作未定义任何变量且不包含任何指令,则该操作可以用简写形式表示,该形式省略了 query 关键字和 query 名称。
例如,此未命名的 query 操作是通过 query 简写编写的。
Example № 6{
field
}
操作选择它需要的信息集,并将准确接收该信息,不多也不少,从而避免过度获取和获取不足数据。
Example № 7{
id
firstName
lastName
}
在此查询中,id
、firstName
和 lastName
字段形成一个选择集。选择集也可能包含片段引用。
选择集主要由字段组成。字段描述了可以在选择集中请求的一个离散信息片段。
一些字段描述了复杂的数据或与其他数据的关系。为了进一步探索这些数据,字段本身可以包含一个选择集,从而允许深度嵌套的请求。所有 GraphQL 操作都必须将其选择指定到返回标量值的字段,以确保明确形状的响应。
例如,此操作选择复杂数据和关系的字段,直到标量值。
Example № 8{
me {
id
firstName
lastName
birthday {
month
day
}
friends {
name
}
}
}
操作的顶层选择集中的字段通常表示应用程序及其当前查看者全局可访问的某些信息。这些顶层字段的一些典型示例包括对当前登录查看器的引用,或访问由唯一标识符引用的某些类型的数据。
Example № 9# `me` could represent the currently logged in viewer.
{
me {
name
}
}
# `user` represents one of many users in a graph of data, referred to by a
# unique identifier.
{
user(id: 4) {
name
}
}
字段在概念上是返回值的函数,有时接受更改其行为的参数。这些参数通常直接映射到 GraphQL 服务器实现中的函数参数。
在此示例中,我们想要查询特定用户(通过 id
参数请求)及其特定 size
的个人资料图片
Example № 10{
user(id: 4) {
id
name
profilePic(size: 100)
}
}
给定字段可以存在许多参数
Example № 11{
user(id: 4) {
id
name
profilePic(width: 100, height: 50)
}
}
参数是无序的
可以以任何语法顺序提供参数,并保持相同的语义含义。
这两个查询在语义上是相同的
Example № 12{
picture(width: 200, height: 100)
}
Example № 13{
picture(height: 100, width: 200)
}
默认情况下,响应对象中的键将使用查询的字段名称。但是,您可以通过指定别名来定义不同的名称。
在此示例中,我们可以获取两种不同尺寸的个人资料图片,并确保生成的对象不会有重复的键
Example № 14{
user(id: 4) {
id
name
smallPic: profilePic(size: 64)
bigPic: profilePic(size: 1024)
}
}
返回结果如下
Example № 15{
"user": {
"id": 4,
"name": "Mark Zuckerberg",
"smallPic": "https://cdn.site.io/pic-4-64.jpg",
"bigPic": "https://cdn.site.io/pic-4-1024.jpg"
}
}
由于查询的顶层是一个字段,因此也可以为其指定别名
Example № 16{
zuck: user(id: 4) {
id
name
}
}
返回结果如下
Example № 17{
"zuck": {
"id": 4,
"name": "Mark Zuckerberg"
}
}
如果提供了别名,则字段的响应键是其别名;否则,它是字段的名称。
片段是 GraphQL 中组合的主要单元。
片段允许重用常用的重复字段选择,从而减少文档中的重复文本。当针对接口或联合类型进行查询时,内联片段可以直接在选择中使用,以根据类型条件进行条件化。
例如,如果我们想获取关于共同朋友以及某些用户的朋友的一些常见信息
Example № 18query noFragments {
user(id: 4) {
friends(first: 10) {
id
name
profilePic(size: 50)
}
mutualFriends(first: 10) {
id
name
profilePic(size: 50)
}
}
}
重复的字段可以提取到一个片段中,并由父片段或查询组合。
Example № 19query withFragments {
user(id: 4) {
friends(first: 10) {
...friendFields
}
mutualFriends(first: 10) {
...friendFields
}
}
}
fragment friendFields on User {
id
name
profilePic(size: 50)
}
片段通过使用扩展运算符 (...
) 来使用。片段选择的所有字段都将添加到与片段调用位于同一级别的查询字段选择中。这通过多个级别的片段扩展来实现。
例如
Example № 20query withNestedFragments {
user(id: 4) {
friends(first: 10) {
...friendFields
}
mutualFriends(first: 10) {
...friendFields
}
}
}
fragment friendFields on User {
id
name
...standardProfilePic
}
fragment standardProfilePic on User {
profilePic(size: 50)
}
查询 noFragments
、withFragments
和 withNestedFragments
都产生相同的响应对象。
片段必须指定它们应用到的类型。在此示例中,friendFields
可以在查询 User
的上下文中使用。
片段不能在任何输入值(标量、枚举或输入对象)上指定。
片段可以在对象类型、接口和联合类型上指定。
仅当片段操作的对象的具体类型与片段的类型匹配时,片段内的选择才会返回值。
例如,在此 Facebook 数据模型上的查询中
Example № 21query FragmentTyping {
profiles(handles: ["zuck", "cocacola"]) {
handle
...userFragment
...pageFragment
}
}
fragment userFragment on User {
friends {
count
}
}
fragment pageFragment on Page {
likers {
count
}
}
profiles
根字段返回一个列表,其中每个元素可以是 Page
或 User
。当 profiles
结果中的对象是 User
时,将存在 friends
,而 likers
将不存在。相反,当结果是 Page
时,将存在 likers
,而 friends
将不存在。
Example № 22{
"profiles": [
{
"handle": "zuck",
"friends": { "count" : 1234 }
},
{
"handle": "cocacola",
"likers": { "count" : 90234512 }
}
]
}
片段可以在选择集中内联定义。这样做是为了根据运行时类型有条件地包含字段。标准片段包含的此功能在 query FragmentTyping
示例中得到了演示。我们可以使用内联片段完成同样的事情。
Example № 23query inlineFragmentTyping {
profiles(handles: ["zuck", "cocacola"]) {
handle
... on User {
friends {
count
}
}
... on Page {
likers {
count
}
}
}
}
内联片段也可以用于将指令应用于一组字段。如果省略了 TypeCondition,则内联片段被视为与封闭上下文的类型相同。
Example № 24query inlineFragmentNoType($expandedInfo: Boolean) {
user(handle: "zuck") {
id
name
... @include(if: $expandedInfo) {
firstName
lastName
birthday
}
}
}
字段和指令参数接受各种字面量基本类型的输入值;输入值可以是标量、枚举值、列表或输入对象。
如果未定义为常量(例如,在 默认值 中),则输入值可以指定为变量。列表和输入对象也可以包含变量(除非定义为常量)。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
整数值是没有小数点或指数的数字(例如 1
)。
e | E |
+ | - |
浮点数值包括小数点(例如 1.0
)或指数(例如 1e50
)或两者(例如 6.0221413e23
)。
true | false |
关键字 true
和 false
表示两个布尔值。
" | \ | / | b | f | n | r | t |
字符串是用双引号 ("
) 包裹的字符序列。(例如 "Hello World"
)。空格和其他原本被忽略的字符在字符串值中具有意义。
块字符串
块字符串是用三引号 ("""
) 包裹的字符序列。空格、行终止符、引号和反斜杠字符都可以不转义地使用,以启用原文文本。字符必须都是有效的 源字符。
由于块字符串表示通常在缩进位置使用的自由格式文本,因此块字符串的字符串值语义通过 BlockStringValue() 排除统一的缩进以及初始和尾随的空白行。
例如,以下包含块字符串的操作
Example № 25mutation {
sendEmail(message: """
Hello,
World!
Yours,
GraphQL.
""")
}
与标准引号字符串相同
Example № 26mutation {
sendEmail(message: "Hello,\n World!\n\nYours,\n GraphQL.")
}
由于块字符串值会去除前导和尾随的空行,因此对于给定的值,没有唯一的规范打印块字符串。由于块字符串通常表示自由格式文本,因此如果它们以空行开始和结束,则认为更易于阅读。
Example № 27"""
This starts with and ends with an empty line,
which makes it easier to read.
"""
Counter Example № 28"""This does not start with or end with any empty lines,
which makes it a little harder to read."""
语义
转义字符 | 代码单元值 | 字符名称 |
---|---|---|
" | U+0022 | 双引号 |
\ | U+005C | 反斜线 |
/ | U+002F | 正斜线 |
b | U+0008 | 退格 |
f | U+000C | 换页 |
n | U+000A | 换行符(新行) |
r | U+000D | 回车符 |
t | U+0009 | 水平制表符 |
空值表示为关键字 null。
GraphQL 有两种语义上不同的方式来表示缺少值
例如,以下两个字段调用类似,但不完全相同
Example № 29{
field(arg: null)
field
}
第一个显式地为参数 “arg” 提供了 null,而第二个隐式地没有为参数 “arg” 提供值。这两种形式的解释可能不同。例如,一个突变表示删除一个字段,而另一个表示不更改一个字段。这两种形式都不能用于期望 Non‐Null 类型的输入。
枚举值表示为不带引号的名称(例如 MOBILE_WEB
)。建议枚举值使用“全大写”。枚举值仅在已知精确枚举类型的上下文中使用。因此,无需在字面量中提供枚举类型名称。
列表是用方括号 [ ]
包裹的有序值序列。列表字面量的值可以是任何值字面量或变量(例如 [1, 2, 3]
)。
逗号在整个 GraphQL 中是可选的,因此允许尾随逗号,重复的逗号不表示缺少值。
语义
输入对象字面量值是用花括号 { }
包裹的键值对输入值的无序列表。对象字面量的值可以是任何输入值字面量或变量(例如 { name: "Hello world", score: 1.0 }
)。我们将输入对象的字面量表示称为“对象字面量”。
输入对象字段是无序的
输入对象字段可以以任何语法顺序提供,并保持相同的语义含义。
这两个查询在语义上是相同的
Example № 30{
nearestThing(location: { lon: 12.43, lat: -53.211 })
}
Example № 31{
nearestThing(location: { lat: -53.211, lon: 12.43 })
}
语义
GraphQL 查询可以使用变量进行参数化,从而最大限度地提高查询重用率,并避免在客户端运行时进行代价高昂的字符串构建。
如果未定义为常量(例如,在 默认值 中),则可以为输入值提供 变量。
变量必须在操作的顶部定义,并且在整个操作执行过程中都在作用域内。
在此示例中,我们希望根据特定设备的大小获取个人资料图片大小
Example № 32query getZuckProfile($devicePicSize: Int) {
user(id: 4) {
id
name
profilePic(size: $devicePicSize)
}
}
这些变量的值与请求一起提供给 GraphQL 服务,以便在执行期间可以替换它们。如果为变量的值提供 JSON,我们可以运行此查询并请求宽度为 60
的 profilePic
Example № 33{
"devicePicSize": 60
}
片段内的变量使用
查询变量可以在片段内使用。查询变量在给定的操作中具有全局作用域,因此在片段中使用的变量必须在任何传递使用该片段的顶层操作中声明。如果变量在片段中被引用,并且包含在一个未定义该变量的操作中,则该操作无法执行。
GraphQL 描述了查询变量期望的数据类型。输入类型可以是另一种输入类型的列表,或者任何其他输入类型的非空变体。
语义
指令提供了一种在 GraphQL 文档中描述备用运行时执行和类型验证行为的方法。
在某些情况下,您需要提供选项来更改 GraphQL 的执行行为,而字段参数不足以满足这些需求,例如有条件地包含或跳过字段。指令通过向执行器描述附加信息来提供此功能。
指令具有名称以及参数列表,这些参数可以接受任何输入类型的值。
指令可用于描述类型、字段、片段和操作的附加信息。
随着未来版本的 GraphQL 采用新的可配置执行能力,它们可能会通过指令公开。
GraphQL 类型系统描述了 GraphQL 服务器的功能,并用于确定查询是否有效。类型系统还描述了查询变量的输入类型,以确定运行时提供的值是否有效。
GraphQL 语言包含一个 IDL,用于描述 GraphQL 服务的类型系统。工具可以使用此定义语言来提供实用程序,例如客户端代码生成或服务引导。
仅寻求提供 GraphQL 查询执行的 GraphQL 工具可以选择不解析 类型系统定义。
包含 类型系统定义 的 GraphQL 文档不得执行;接收包含类型系统定义的 GraphQL 文档的 GraphQL 执行服务应返回描述性错误。
类型系统扩展用于表示从某些原始类型系统扩展而来的 GraphQL 类型系统。例如,本地服务可以使用它来表示 GraphQL 客户端仅在本地访问的数据,或者 GraphQL 服务本身是另一个 GraphQL 服务的扩展。
GraphQL 服务的集体类型系统功能被称为该服务的“schema”。schema 是根据它支持的类型和指令以及每种操作(查询、突变和订阅)的根操作类型来定义的;这决定了这些操作在类型系统中开始的位置。
GraphQL schema 本身必须是内部有效的。本节描述了与此验证过程相关的规则。
GraphQL schema 中的所有类型都必须具有唯一的名称。提供的任何两个类型都不得具有相同的名称。提供的任何类型都不得具有与任何内置类型(包括标量和内省类型)冲突的名称。
GraphQL schema 中的所有指令都必须具有唯一的名称。
schema 中定义的所有类型和指令的名称都不得以 "__"(两个下划线)开头,因为这专门用于 GraphQL 的内省系统。
schema 为其支持的每种操作(查询、突变和订阅)定义初始根操作类型;这决定了这些操作在类型系统中开始的位置。
必须提供 query
根操作类型,并且必须是对象类型。
mutation
根操作类型是可选的;如果未提供,则服务不支持突变。如果提供了,则必须是对象类型。
类似地,subscription
根操作类型也是可选的;如果未提供,则服务不支持订阅。如果提供了,则必须是对象类型。
query
根操作类型上的字段指示在 GraphQL 查询的顶层可用的字段。例如,基本的 GraphQL 查询,如
Example № 34query {
myName
}
当 query
根操作类型具有名为 “myName” 的字段时有效。
Example № 35type Query {
myName: String
}
同样地,如果 mutation
根操作类型有一个名为“setName”的字段,则以下 mutation 是有效的。请注意,query
和 mutation
根类型必须是不同的类型。
Example № 36mutation {
setName(name: "Zuck") {
newName
}
}
当使用类型系统定义语言时,一个文档最多只能包含一个 schema
定义。
在此示例中,GraphQL 模式定义了 query 和 mutation 根类型
Example № 37schema {
query: MyQueryRootType
mutation: MyMutationRootType
}
type MyQueryRootType {
someField: String
}
type MyMutationRootType {
setSomeField(to: String): String
}
默认根操作类型名称
虽然任何类型都可以作为 GraphQL 操作的根操作类型,但当 query
、mutation
和 subscription
根类型分别命名为 Query
、Mutation
和 Subscription
时,类型系统定义语言可以省略 schema 定义。
同样地,当使用类型系统定义语言表示 GraphQL 模式时,如果模式仅使用默认根操作类型名称,则应省略 schema 定义。
此示例描述了一个有效的完整 GraphQL 模式,尽管未显式包含 schema
定义。Query
类型被推定为模式的 query
根操作类型。
Example № 38type Query {
someField: String
}
模式扩展用于表示从原始模式扩展而来的模式。例如,GraphQL 服务可能会使用它来向现有模式添加额外的操作类型或额外的指令。
模式验证
如果模式扩展定义不正确,则可能无效。
文档是 GraphQL 类型系统的一流特性。为了确保 GraphQL 服务的文档与其功能保持一致,GraphQL 定义的描述与其定义一起提供,并通过内省提供。
为了使 GraphQL 服务设计者能够轻松地发布与其 GraphQL 服务功能相关的文档,GraphQL 描述使用 Markdown 语法(如 CommonMark 所指定)定义。在类型系统定义语言中,这些描述字符串(通常是 BlockString)出现在它们描述的定义之前。
所有 GraphQL 类型、字段、参数和其他可以描述的定义都应提供 Description,除非它们被认为是自描述的。
例如,这个简单的 GraphQL 模式就得到了很好的描述
Example № 39"""
A simple GraphQL schema which is well described.
"""
type Query {
"""
Translates a string from a given language into a different language.
"""
translate(
"The original language that `text` is provided in."
fromLanguage: Language
"The translated language to be returned."
toLanguage: Language
"The text to be translated."
text: String
): String
}
"""
The set of languages supported by `translate`.
"""
enum Language {
"English"
EN
"French"
FR
"Chinese"
CH
}
任何 GraphQL 模式的基本单元都是类型。GraphQL 中有六种命名的类型定义和两种包装类型。
最基本的类型是 Scalar
。标量表示原始值,例如字符串或整数。通常,标量字段的可能响应是可枚举的。GraphQL 在这些情况下提供 Enum
类型,其中该类型指定了有效响应的空间。
标量和枚举构成了响应树中的叶子节点;中间层是 Object
类型,它定义了一组字段,其中每个字段都是系统中的另一种类型,从而允许定义任意类型层次结构。
GraphQL 支持两种抽象类型:接口和联合。
Interface
定义了一个字段列表;实现该接口的 Object
类型保证实现这些字段。每当类型系统声明它将返回一个接口时,它将返回一个有效的实现类型。
Union
定义了一个可能的类型列表;与接口类似,每当类型系统声明将返回一个联合时,将返回其中一种可能的类型。
最后,通常将复杂结构作为 GraphQL 字段参数或变量的输入非常有用;Input Object
类型允许模式精确定义期望的数据。
到目前为止,所有类型都被假定为既可为空又为单数:例如,标量字符串返回 null 或单个字符串。
GraphQL 模式可以描述一个字段表示另一种类型的列表;为此提供了 List
类型,并包装了另一种类型。
类似地,Non-Null
类型包装了另一种类型,并表示结果值永远不会是 null(并且错误不会导致 null 值)。
这两种类型被称为“包装类型”;非包装类型被称为“命名类型”。包装类型具有一个底层命名类型,通过不断解包类型直到找到命名类型为止。
类型在整个 GraphQL 中用于描述作为参数和变量输入接受的值,以及字段输出的值。这两种用途将类型分类为输入类型和输出类型。某些类型的类型,如标量和枚举类型,既可以用作输入类型,也可以用作输出类型;其他类型的类型只能用于其中一种。输入对象类型只能用作输入类型。对象、接口和联合类型只能用作输出类型。列表和非空类型可以用作输入类型或输出类型,具体取决于包装类型的使用方式。
类型扩展用于表示从某些原始类型扩展而来的 GraphQL 类型。例如,本地服务可以使用它来表示 GraphQL 客户端仅在本地访问的额外字段。
标量类型表示 GraphQL 类型系统中的原始叶子值。GraphQL 响应采用分层树的形式;这些树上的叶子是 GraphQL 标量。
所有 GraphQL 标量都可以表示为字符串,尽管根据所使用的响应格式,对于给定的标量类型可能存在更合适的原始类型,服务器应在适当时使用这些类型。
GraphQL 提供了许多内置标量,但类型系统可以添加具有语义含义的额外标量。例如,GraphQL 系统可以定义一个名为 Time
的标量,它虽然序列化为字符串,但承诺符合 ISO‐8601。当查询 Time
类型的字段时,您可以依赖使用 ISO‐8601 解析器解析结果并为时间使用特定于客户端的原始类型的功能。另一个可能有用的自定义标量的例子是 Url
,它序列化为字符串,但服务器保证它是有效的 URL。
Example № 40scalar Time
scalar Url
服务器可以从其模式中省略任何内置标量,例如,如果模式不引用浮点数,则它不得包含 Float
类型。但是,如果模式包含一个名称与此处描述的类型之一相同的类型,则它必须遵守所描述的行为。例如,服务器不得包含名为 Int
的类型并使用它来表示 128 位数字、国际化信息或本文档中定义以外的任何内容。
当使用类型系统定义语言表示 GraphQL 模式时,为了简洁起见,应省略内置标量类型。
结果强制转换
GraphQL 服务器在准备给定标量类型的字段时,必须遵守标量类型描述的约定,通过强制转换值或在无法强制转换值或强制转换可能导致数据丢失时生成字段错误。
GraphQL 服务可以决定允许将不同的内部类型强制转换为预期的返回类型。例如,当强制转换 Int
类型的字段时,布尔值 true
可能会生成 1
,或者字符串值 "123"
可能会被解析为十进制 123
。但是,如果内部类型强制转换在不丢失信息的情况下无法合理执行,则必须引发字段错误。
由于客户端无法观察到这种强制转换行为,因此强制转换的精确规则留给实现来决定。唯一的要求是服务器必须生成符合预期标量类型的值。
输入强制转换
如果 GraphQL 服务器期望标量类型作为参数的输入,则强制转换是可观察的,并且规则必须明确定义。如果输入值与强制转换规则不匹配,则必须引发查询错误。
GraphQL 具有不同的常量字面量来表示整数和浮点输入值,并且强制转换规则可能因遇到的输入值类型而异。GraphQL 可以通过查询变量进行参数化,查询变量的值通常在通过 HTTP 等传输发送时进行序列化。由于某些常见的序列化(例如 JSON)不区分整数值和浮点值,因此如果它们具有空的小数部分(例如 1.0
),则将它们解释为整数输入值,否则将其解释为浮点输入值。
对于以下所有类型,除了 Non‐Null 之外,如果提供了显式值 null,则输入强制转换的结果为 null。
内置标量
GraphQL 提供了一组基本的、定义良好的标量类型。GraphQL 服务器应支持所有这些类型,并且提供这些名称类型的 GraphQL 服务器必须遵守以下描述的行为。
Int 标量类型表示有符号 32 位数字非小数数值。支持 32 位整数或数字类型的响应格式应使用该类型来表示此标量。
结果强制转换
返回 Int
类型的字段期望遇到 32 位整数内部值。
当 GraphQL 服务器在不丢失信息的情况下合理地将非整数内部值强制转换为整数时,它们可能会这样做,否则它们必须引发字段错误。这方面的例子可能包括为浮点数 1.0
返回 1
,或为字符串 "123"
返回 123
。在强制转换可能丢失数据的情况下,引发字段错误更为合适。例如,浮点数 1.2
应引发字段错误,而不是被截断为 1
。
如果整数内部值表示小于 -231 或大于或等于 231 的值,则应引发字段错误。
输入强制转换
当预期作为输入类型时,仅接受整数输入值。所有其他输入值,包括具有数字内容的字符串,都必须引发查询错误,指示类型不正确。如果整数输入值表示小于 -231 或大于或等于 231 的值,则应引发查询错误。
Float 标量类型表示由 IEEE 754 指定的有符号双精度小数数值。支持适当双精度数字类型的响应格式应使用该类型来表示此标量。
结果强制转换
返回 Float
类型的字段期望遇到双精度浮点内部值。
当 GraphQL 服务器在不丢失信息的情况下合理地将非浮点内部值强制转换为 Float
时,它们可能会这样做,否则它们必须引发字段错误。这方面的例子可能包括为整数 1
返回 1.0
,或为字符串 "123"
返回 123.0
。
输入强制转换
当预期作为输入类型时,整数和浮点输入值都被接受。整数输入值通过添加空的小数部分强制转换为 Float,例如,整数输入值 1
的值为 1.0
。所有其他输入值,包括具有数字内容的字符串,都必须引发查询错误,指示类型不正确。如果整数输入值表示 IEEE 754 无法表示的值,则应引发查询错误。
String 标量类型表示文本数据,表示为 UTF‐8 字符序列。String 类型最常被 GraphQL 用于表示自由形式的人类可读文本。所有响应格式都必须支持字符串表示形式,并且此处必须使用该表示形式。
结果强制转换
返回 String
类型的字段期望遇到 UTF‐8 字符串内部值。
当 GraphQL 服务器在不丢失信息的情况下合理地将非字符串原始值强制转换为 String
时,它们可能会这样做,否则它们必须引发字段错误。这方面的例子可能包括为布尔值 true 返回字符串 "true"
,或为整数 1
返回字符串 "1"
。
输入强制转换
当预期作为输入类型时,仅接受有效的 UTF‐8 字符串输入值。所有其他输入值都必须引发查询错误,指示类型不正确。
Boolean 标量类型表示 true
或 false
。响应格式应使用内置布尔类型(如果支持);否则,它们应使用整数 1
和 0
的表示形式。
结果强制转换
返回 Boolean
类型的字段期望遇到布尔内部值。
当 GraphQL 服务器在不丢失信息的情况下合理地将非布尔原始值强制转换为 Boolean
时,它们可能会这样做,否则它们必须引发字段错误。这方面的例子可能包括为非零数字返回 true
。
输入强制转换
当预期作为输入类型时,仅接受布尔输入值。所有其他输入值都必须引发查询错误,指示类型不正确。
ID 标量类型表示唯一标识符,通常用于重新获取对象或作为缓存的键。ID 类型的序列化方式与 String
相同;但是,它不应该是人类可读的。虽然它通常是数字,但它应始终序列化为 String
。
结果强制转换
GraphQL 与 ID 格式无关,并序列化为字符串以确保 ID 可能表示的多种格式的一致性,从小的自动递增数字到大的 128 位随机数,到 base64 编码值,或格式如 GUID 的字符串值。
GraphQL 服务器应根据它们期望的 ID 格式进行适当的强制转换。当无法进行强制转换时,它们必须引发字段错误。
输入强制转换
当预期作为输入类型时,任何字符串(例如 "4"
)或整数(例如 4
)输入值都应根据给定 GraphQL 服务器期望的 ID 格式强制转换为 ID。任何其他输入值,包括浮点输入值(例如 4.0
),都必须引发查询错误,指示类型不正确。
标量类型扩展用于表示从某些原始标量类型扩展而来的标量类型。例如,GraphQL 工具或服务可以使用它来向现有标量添加指令。
类型验证
如果标量类型扩展定义不正确,则可能无效。
GraphQL 查询是分层和组合的,描述了一个信息树。虽然标量类型描述了这些分层查询的叶子值,但对象描述了中间层。
GraphQL 对象表示命名字段的列表,每个字段都产生特定类型的值。对象值应序列化为有序映射,其中查询的字段名称(或别名)是键,而评估字段的结果是值,按它们在查询中出现的顺序排序。
在对象类型中定义的所有字段都不得具有以 "__"(两个下划线)开头的名称,因为这由 GraphQL 的内省系统独占使用。
例如,类型 Person
可以描述为
Example № 41type Person {
name: String
age: Int
picture: Url
}
其中 name
是一个将产生 String
值的字段,而 age
是一个将产生 Int
值的字段,而 picture
是一个将产生 Url
值的字段。
对象值的查询必须至少选择一个字段。此字段选择将产生一个有序映射,其中包含对象查询的确切子集,该子集应以它们被查询的顺序表示。只有在对象类型上声明的字段才能在该对象上进行有效查询。
例如,选择 Person
的所有字段
Example № 42{
name
age
picture
}
将产生对象
Example № 43{
"name": "Mark Zuckerberg",
"age": 30,
"picture": "http://some.cdn/picture.jpg"
}
而选择字段的子集
Example № 44{
age
name
}
必须只产生该子集
Example № 45{
"age": 30,
"name": "Mark Zuckerberg"
}
对象类型的字段可以是标量、枚举、另一个对象类型、接口或联合。此外,它可以是任何包装类型,其底层基本类型是这五种类型之一。
例如,Person
类型可能包含 relationship
Example № 46type Person {
name: String
age: Int
picture: Url
relationship: Person
}
有效查询必须为返回对象的字段提供嵌套字段集,因此此查询无效
Counter Example № 47{
name
relationship
}
但是,此示例有效
Example № 48{
name
relationship {
name
}
}
并将产生每个查询对象类型的子集
Example № 49{
"name": "Mark Zuckerberg",
"relationship": {
"name": "Priscilla Chan"
}
}
字段排序
当查询对象时,字段的结果映射在概念上按照它们在查询执行期间遇到的相同顺序排序,不包括类型不适用的片段以及通过 @skip
或 @include
指令跳过的字段或片段。当使用 CollectFields() 算法时,可以正确生成此排序。
能够表示有序映射的响应序列化格式应保持此排序。只能表示无序映射的序列化格式(例如 JSON)应以文本形式保留此顺序。也就是说,如果按 {foo, bar}
的顺序查询了两个字段,则生成的 JSON 序列化应包含 {"foo": "...", "bar": "..."}
,顺序相同。
生成响应,其中字段以它们在请求中出现的相同顺序表示,可以提高调试期间的人类可读性,并且如果可以预测属性的顺序,则可以更有效地解析响应。
如果片段在其他字段之前展开,则该片段指定的字段在响应中出现在后续字段之前。
Example № 50{
foo
...Frag
qux
}
fragment Frag on Query {
bar
baz
}
产生有序结果
Example № 51{
"foo": 1,
"bar": 2,
"baz": 3,
"qux": 4
}
如果在选择中多次查询一个字段,则按第一次遇到它的时间排序。但是,类型不适用的片段不会影响排序。
Example № 52{
foo
...Ignored
...Matching
bar
}
fragment Ignored on UnknownType {
qux
baz
}
fragment Matching on Query {
bar
qux
foo
}
产生有序结果
Example № 53{
"foo": 1,
"bar": 2,
"qux": 3
}
此外,如果指令导致字段被排除,则在字段排序中不考虑它们。
Example № 54{
foo @skip(if: true)
bar
foo
}
产生有序结果
Example № 55{
"bar": 1,
"foo": 2
}
结果强制转换
确定强制转换对象的结果是 GraphQL 执行器的核心,因此这在规范的该部分中介绍。
输入强制转换
对象永远不是有效的输入。
类型验证
如果对象类型定义不正确,则可能无效。GraphQL 模式中的每个对象类型都必须遵守这组规则。
对象字段在概念上是产生值的函数。有时,对象字段可以接受参数以进一步指定返回值。对象字段参数被定义为所有可能的参数名称及其预期输入类型的列表。
字段中定义的所有参数都不得具有以 "__"(两个下划线)开头的名称,因为这由 GraphQL 的内省系统独占使用。
例如,具有 picture
字段的 Person
类型可以接受一个参数来确定要返回的图像大小。
Example № 56type Person {
name: String
picture(size: Int): Url
}
GraphQL 查询可以选择性地为其字段指定参数以提供这些参数。
此示例查询
Example № 57{
name
picture(size: 600)
}
可能产生结果
Example № 58{
"name": "Mark Zuckerberg",
"picture": "http://some.cdn/picture_600.jpg"
}
对象字段参数的类型必须是输入类型(对象、接口或联合类型以外的任何类型)。
对象中的字段可以根据应用程序的需要标记为已弃用。查询这些字段仍然是合法的(以确保现有客户端不会因更改而中断),但应在文档和工具中对这些字段进行适当处理。
当使用类型系统定义语言时,@deprecated
指令用于指示字段已被弃用
Example № 59type ExampleType {
oldField: String @deprecated
}
对象类型扩展用于表示从某些原始类型扩展而来的类型。例如,这可以用于表示本地数据,或者由本身是另一个 GraphQL 服务的扩展的 GraphQL 服务使用。
在此示例中,本地数据字段被添加到 Story
类型
Example № 60extend type Story {
isHiddenLocally: Boolean
}
对象类型扩展可以选择不添加额外的字段,而是仅添加接口或指令。
在此示例中,指令被添加到 User
类型,而没有添加字段
Example № 61extend type User @addedDirective
类型验证
如果对象类型扩展定义不正确,则可能无效。
GraphQL 接口表示命名字段及其参数的列表。然后,GraphQL 对象可以实现这些接口,这要求对象类型将定义这些接口定义的所有字段。
GraphQL 接口上的字段与 GraphQL 对象上的字段具有相同的规则;它们的类型可以是标量、对象、枚举、接口或联合,或者任何包装类型,其基本类型是这五种之一。
例如,接口 NamedEntity
可以描述一个必需的字段,而诸如 Person
或 Business
之类的类型可以实现此接口,以保证此字段将始终存在。
类型也可以实现多个接口。例如,在下面的示例中,Business
同时实现了 NamedEntity
和 ValuedEntity
接口。
Example № 62interface NamedEntity {
name: String
}
interface ValuedEntity {
value: Int
}
type Person implements NamedEntity {
name: String
age: Int
}
type Business implements NamedEntity & ValuedEntity {
name: String
value: Int
employeeCount: Int
}
当预期有多个对象类型,但应保证某些字段时,产生接口的字段很有用。
为了继续该示例,Contact
可能引用 NamedEntity
。
Example № 63type Contact {
entity: NamedEntity
phoneNumber: String
address: String
}
这允许我们为 Contact
编写查询,该查询可以选择通用字段。
Example № 64{
entity {
name
}
phoneNumber
}
当查询接口类型上的字段时,只能查询在该接口上声明的字段。在上面的示例中,entity
返回 NamedEntity
,并且 name
在 NamedEntity
上定义,因此它是有效的。但是,以下查询将无效
Counter Example № 65{
entity {
name
age
}
phoneNumber
}
因为 entity
引用 NamedEntity
,而 age
未在该接口上定义。仅当 entity
的结果为 Person
时,查询 age
才有效;查询可以使用片段或内联片段来表达这一点
Example № 66{
entity {
name
... on Person {
age
}
},
phoneNumber
}
结果强制转换
接口类型应具有某种方式来确定给定结果对应于哪个对象。一旦完成,接口的结果强制转换与对象的结果强制转换相同。
输入强制转换
接口永远不是有效的输入。
类型验证
如果接口类型定义不正确,则可能无效。
接口类型扩展用于表示从某些原始接口扩展而来的接口。例如,这可以用于表示许多类型上的通用本地数据,或者由本身是另一个 GraphQL 服务的扩展的 GraphQL 服务使用。
在此示例中,扩展数据字段被添加到 NamedEntity
类型以及实现它的类型
Example № 67extend interface NamedEntity {
nickname: String
}
extend type Person {
nickname: String
}
extend type Business {
nickname: String
}
接口类型扩展可以选择不添加额外的字段,而是仅添加指令。
在此示例中,指令被添加到 NamedEntity
类型,而没有添加字段
Example № 68extend interface NamedEntity @addedDirective
类型验证
如果接口类型扩展定义不正确,则可能无效。
GraphQL 联合类型表示一个对象,它可以是 GraphQL 对象类型列表中的一种,但不提供这些类型之间任何保证的字段。它们也与接口不同,因为对象类型声明它们实现了哪些接口,但不知道哪些联合类型包含它们。
对于接口和对象,只能直接查询在该类型上定义的字段;要查询接口上的其他字段,必须使用类型化的片段。这与联合类型相同,但是联合类型不定义任何字段,因此没有字段可以在不使用类型细化片段或内联片段的情况下在此类型上查询。
例如,我们可以定义以下类型
Example № 69union SearchResult = Photo | Person
type Person {
name: String
age: Int
}
type Photo {
height: Int
width: Int
}
type SearchQuery {
firstSearchResult: SearchResult
}
当查询 SearchQuery
类型的 firstSearchResult
字段时,查询将要求片段内部的所有字段,指示适当的类型。如果查询想要在结果是 Person 时获取名称,并在结果是照片时获取高度,则以下查询无效,因为联合类型本身未定义任何字段
Counter Example № 70{
firstSearchResult {
name
height
}
}
相反,查询将是
Example № 71{
firstSearchResult {
... on Person {
name
}
... on Photo {
height
}
}
}
联合类型的成员可以使用可选的前导 |
字符定义,以帮助在表示更长的可能类型列表时进行格式化
Example № 72union SearchResult =
| Photo
| Person
结果强制转换
联合类型应具有某种方式来确定给定结果对应于哪个对象。一旦完成,联合类型的结果强制转换与对象的结果强制转换相同。
输入强制转换
联合类型永远不是有效的输入。
类型验证
如果联合类型定义不正确,则可能无效。
联合类型扩展用于表示从某些原始联合类型扩展而来的联合类型。例如,这可以用于表示额外的本地数据,或者由本身是另一个 GraphQL 服务的扩展的 GraphQL 服务使用。
类型验证
如果联合类型扩展定义不正确,则可能无效。
GraphQL 枚举类型,与标量类型一样,也表示 GraphQL 类型系统中的叶值。但是,枚举类型描述了可能值的集合。
枚举不是数值的引用,而是其自身唯一的价值。它们可以序列化为字符串:表示值的名称。
在此示例中,定义了一个名为 Direction
的枚举类型
Example № 73enum Direction {
NORTH
EAST
SOUTH
WEST
}
结果强制转换
GraphQL 服务器必须返回定义的可能值集之一。如果无法进行合理的强制转换,则它们必须引发字段错误。
输入强制转换
GraphQL 具有常量字面量来表示枚举输入值。GraphQL 字符串字面量不得被接受为枚举输入,而应引发查询错误。
对于非字符串符号值具有不同表示形式的查询变量传输序列化(例如,EDN)应仅允许此类值作为枚举输入值。否则,对于大多数不这样做的传输序列化,字符串可以解释为具有相同名称的枚举输入值。
类型验证
如果枚举类型定义不正确,则可能无效。
枚举类型扩展用于表示从某些原始枚举类型扩展而来的枚举类型。例如,这可以用于表示额外的本地数据,或者由本身是另一个 GraphQL 服务的扩展的 GraphQL 服务使用。
类型验证
如果枚举类型扩展定义不正确,则可能无效。
字段可以接受参数来配置其行为。这些输入通常是标量或枚举,但它们有时需要表示更复杂的值。
GraphQL 输入对象定义了一组输入字段;输入字段可以是标量、枚举或其他输入对象。这允许参数接受任意复杂的结构。
在此示例中,名为 Point2D
的输入对象描述了 x
和 y
输入
Example № 74input Point2D {
x: Float
y: Float
}
结果强制转换
输入对象永远不是有效的结果。输入对象类型不能是对象或接口字段的返回类型。
输入强制转换
输入对象的值应为输入对象字面量或由变量提供无序映射,否则必须抛出查询错误。在任何一种情况下,输入对象字面量或无序映射都不得包含任何名称不由该输入对象类型的字段定义的条目,否则必须抛出错误。
强制转换的结果是一个无序映射,其中包含输入对象类型定义的每个字段的条目,以及存在值的字段的条目。结果映射使用以下规则构建
以下是输入对象类型的输入强制转换示例,其中包含 String
字段 a
和必需的(非空)Int!
字段 b
Example № 75input ExampleInputObject {
a: String
b: Int!
}
字面量值 | 变量 | 强制转换的值 |
---|---|---|
{ a: "abc", b: 123 } | {} | { a: "abc", b: 123 } |
{ a: null, b: 123 } | {} | { a: null, b: 123 } |
{ b: 123 } | {} | { b: 123 } |
{ a: $var, b: 123 } | { var: null } | { a: null, b: 123 } |
{ a: $var, b: 123 } | {} | { b: 123 } |
{ b: $var } | { var: 123 } | { b: 123 } |
$var | { var: { b: 123 } } | { b: 123 } |
"abc123" | {} | 错误:值不正确 |
$var | { var: "abc123" } } | 错误:值不正确 |
{ a: "abc", b: "123" } | {} | 错误:字段 b 的值不正确 |
{ a: "abc" } | {} | 错误:缺少必需字段 b |
{ b: $var } | {} | 错误:缺少必需字段 b。 |
$var | { var: { a: "abc" } } | 错误:缺少必需字段 b |
{ a: "abc", b: null } | {} | 错误:b 必须是非空的。 |
{ b: $var } | { var: null } | 错误:b 必须是非空的。 |
{ b: 123, c: "xyz" } | {} | 错误:意外字段 c |
类型验证
输入对象类型扩展用于表示从某些原始输入对象类型扩展而来的输入对象类型。例如,这可以由本身是另一个 GraphQL 服务的扩展的 GraphQL 服务使用。
类型验证
如果输入对象类型扩展定义不正确,则可能无效。
GraphQL 列表是一种特殊的集合类型,它声明列表中每个项的类型(称为列表的项类型)。列表值序列化为有序列表,其中列表中的每个项都按照项类型进行序列化。要表示字段使用列表类型,项类型用方括号括起来,如下所示:pets: [Pet]
。
结果强制转换
GraphQL 服务器必须返回有序列表作为列表类型的结果。列表中的每个项都必须是项类型的结果强制转换的结果。如果无法进行合理的强制转换,则必须引发字段错误。特别是,如果返回非列表,则强制转换应失败,因为这表明类型系统和实现之间的期望不匹配。
如果列表的项类型是可为空的,则在列表中的单个项的准备或强制转换期间发生的错误必须导致在该列表中的该位置的值为 null,并在响应中添加错误。如果列表的项类型是非空的,则在列表中的单个项上发生的错误必须导致整个列表的字段错误。
输入强制转换
当预期作为输入时,只有当列表中的每个项都可以被列表的项类型接受时,才接受列表值。
如果作为输入传递给列表类型的值不是列表,也不是 null 值,则输入强制转换的结果是大小为 1 的列表,其中单个项值是提供的列表中项类型的输入强制转换的结果(请注意,这可能递归地应用于嵌套列表)。
这允许接受一个或多个参数(有时称为“可变参数”)的输入将其输入类型声明为列表,而在单个值的常见情况下,客户端可以直接传递该值,而不是构造列表。
以下是具有各种列表类型和值的输入强制转换示例
预期类型 | 提供的值 | 强制转换的值 |
---|---|---|
[Int] | [1, 2, 3] | [1, 2, 3] |
[Int] | [1, "b", true] | 错误:项值不正确 |
[Int] | 1 | [1] |
[Int] | null | null |
[[Int]] | [[1], [2, 3]] | [[1], [2, 3] |
[[Int]] | [1, 2, 3] | 错误:项值不正确 |
[[Int]] | 1 | [[1]] |
[[Int]] | null | null |
默认情况下,GraphQL 中的所有类型都是可为空的;null 值对于上述所有类型都是有效的响应。要声明不允许为空的类型,可以使用 GraphQL 非空类型。此类型包装了一个底层类型,并且此类型的行为与该包装类型完全相同,但 null 不是包装类型的有效响应除外。尾随感叹号用于表示使用非空类型的字段,如下所示:name: String!
。
可为空与可选
字段在查询上下文中始终是可选的,可以省略字段,查询仍然有效。但是,如果查询返回非空类型的字段,则永远不会返回 null 值。
输入(例如字段参数)默认情况下始终是可选的。但是,非空输入类型是必需的。除了不接受值 null 之外,它也不接受省略。为了简单起见,可为空的类型始终是可选的,而非空类型始终是必需的。
结果强制转换
在上述所有结果强制转换中,null 被认为是有效值。要强制转换非空类型的结果,应执行包装类型的强制转换。如果该结果不是 null,则强制转换非空类型的结果就是该结果。如果该结果是 null,则必须引发字段错误。
输入强制转换
如果未提供非空类型的参数或输入对象字段,或者提供了字面量值 null,或者提供了运行时未提供值的变量,或者提供了值 null,则必须引发查询错误。
如果提供给非空类型的值是字面量值(而不是 null)或非空变量值,则使用包装类型的输入强制转换对其进行强制转换。
非空参数不能省略
Counter Example № 76{
fieldWithNonNullArg
}
值 null 不能提供给非空参数
Counter Example № 77{
fieldWithNonNullArg(nonNullArg: null)
}
可为空类型的变量不能提供给非空参数
Example № 78query withNullableVariable($var: String) {
fieldWithNonNullArg(nonNullArg: $var)
}
类型验证
列表和非空包装类型可以组合,表示更复杂的类型。列表和非空类型的结果强制转换和输入强制转换规则以递归方式应用。
例如,如果列表的内部项类型是非空的(例如 [T!]
),则该列表可能不包含任何 null 项。但是,如果非空的内部类型是列表(例如 [T]!
),则不接受 null,但接受空列表。
以下是具有各种类型和值的结果强制转换示例
预期类型 | 内部值 | 强制转换的结果 |
---|---|---|
[Int] | [1, 2, 3] | [1, 2, 3] |
[Int] | null | null |
[Int] | [1, 2, null] | [1, 2, null] |
[Int] | [1, 2, 错误] | [1, 2, null] (带有记录的错误) |
[Int]! | [1, 2, 3] | [1, 2, 3] |
[Int]! | null | 错误:值不能为空 |
[Int]! | [1, 2, null] | [1, 2, null] |
[Int]! | [1, 2, 错误] | [1, 2, null] (带有记录的错误) |
[Int!] | [1, 2, 3] | [1, 2, 3] |
[Int!] | null | null |
[Int!] | [1, 2, null] | null (带有记录的强制转换错误) |
[Int!] | [1, 2, 错误] | null (带有记录的错误) |
[Int!]! | [1, 2, 3] | [1, 2, 3] |
[Int!]! | null | 错误:值不能为空 |
[Int!]! | [1, 2, null] | 错误:项不能为空 |
[Int!]! | [1, 2, 错误] | 错误:项中发生错误 |
QUERY |
MUTATION |
SUBSCRIPTION |
FIELD |
FRAGMENT_DEFINITION |
FRAGMENT_SPREAD |
INLINE_FRAGMENT |
SCHEMA |
SCALAR |
OBJECT |
FIELD_DEFINITION |
ARGUMENT_DEFINITION |
INTERFACE |
UNION |
ENUM |
ENUM_VALUE |
INPUT_OBJECT |
INPUT_FIELD_DEFINITION |
GraphQL 模式描述了指令,这些指令用于注释 GraphQL 文档的各个部分,以指示验证器、执行器或客户端工具(例如代码生成器)应以不同的方式对其进行评估。
GraphQL 实现应提供 @skip
和 @include
指令。
如果 GraphQL 实现支持类型系统定义语言,则在表示模式的已弃用部分时,必须提供 @deprecated
指令。
指令必须仅在声明它们所属的位置使用。在此示例中,定义了一个指令,该指令可用于注释片段定义
Example № 79directive @example on FIELD
fragment SomeFragment on SomeType {
field @example
}
指令位置可以使用可选的前导 |
字符定义,以帮助在表示更长的可能位置列表时进行格式化
Example № 80directive @example on
| FIELD
| FRAGMENT_SPREAD
| INLINE_FRAGMENT
指令还可以用于注释类型系统定义语言,这对于提供额外的元数据以生成 GraphQL 执行服务、生成客户端生成的运行时代码或 GraphQL 语义的许多其他有用扩展可能是一个有用的工具。
在此示例中,指令 @example
注释了字段和参数定义
Example № 81directive @example on FIELD_DEFINITION | ARGUMENT_DEFINITION
type SomeType {
field(arg: Int @example): String @example
}
在定义指令时,它不得直接或间接地引用自身
Counter Example № 82directive @invalidExample(arg: String @invalidExample) on ARGUMENT_DEFINITION
验证
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
@skip
指令可以为字段、片段扩展和内联片段提供,并允许在执行期间根据 if 参数的描述进行条件排除。
在此示例中,只有当变量 $someTest
的值为 false
时,才会查询 experimentalField
。
Example № 83query myQuery($someTest: Boolean) {
experimentalField @skip(if: $someTest)
}
directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
@include
指令可以为字段、片段扩展和内联片段提供,并允许在执行期间根据 if 参数的描述进行条件包含。
在此示例中,只有当变量 $someTest
的值为 true
时,才会查询 experimentalField
Example № 84query myQuery($someTest: Boolean) {
experimentalField @include(if: $someTest)
}
@skip
和 @include
指令之间没有优先顺序。如果同一个字段或片段上同时提供了 @skip
和 @include
指令,则必须仅当 @skip
条件为假且 @include
条件为真时才查询。反之,如果 @skip
条件为真或 @include
条件为假,则字段或片段不得被查询。directive @deprecated(
reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE
@deprecated
指令在类型系统定义语言中使用,以指示 GraphQL 服务模式中已弃用的部分,例如类型上已弃用的字段或已弃用的枚举值。
弃用说明包含弃用原因,该原因使用 Markdown 语法格式化(如 CommonMark 所指定)。
在此类型定义示例中,oldField
已被弃用,推荐使用 newField
。
Example № 85type ExampleType {
newField: String
oldField: String @deprecated(reason: "Use `newField`.")
}
GraphQL 服务器支持对其模式进行内省。此模式本身使用 GraphQL 查询,从而为工具构建创建了一个强大的平台。
以一个简单的应用程序的查询示例为例。在这种情况下,有一个 User 类型,包含三个字段:id、name 和 birthday。
例如,给定一个具有以下类型定义的服务器
Example № 86type User {
id: String
name: String
birthday: Date
}
查询
Example № 87{
__type(name: "User") {
name
fields {
name
type {
name
}
}
}
}
将返回
Example № 88{
"__type": {
"name": "User",
"fields": [
{
"name": "id",
"type": { "name": "String" }
},
{
"name": "name",
"type": { "name": "String" }
},
{
"name": "birthday",
"type": { "name": "Date" }
},
]
}
}
GraphQL 内省系统所需的类型和字段,如果与用户定义的类型和字段在同一上下文中使用,则以双下划线 "__" 为前缀。 这是为了避免与用户定义的 GraphQL 类型发生命名冲突。 相反,GraphQL 类型系统作者不得定义任何以两个前导下划线开头的类型、字段、参数或任何其他类型系统工件。
内省系统中的所有类型都提供一个 description
字段,类型为 String
,以便类型设计者可以发布文档以及功能。 GraphQL 服务器可以使用 Markdown 语法(如 CommonMark 所指定)返回 description
字段。 因此,建议任何显示 description
的工具都使用兼容 CommonMark 的 Markdown 渲染器。
为了支持向后兼容性的管理,GraphQL 字段和枚举值可以指示它们是否已弃用 (isDeprecated: Boolean
) 以及弃用原因的描述 (deprecationReason: String
)。
使用 GraphQL 内省构建的工具应通过信息隐藏或面向开发人员的警告来劝阻已弃用的使用,从而尊重弃用。
当针对任何 Object、Interface 或 Union 进行查询时,GraphQL 支持在查询中的任何点通过元字段 __typename: String!
进行类型名称内省。 它返回当前正在查询的对象类型的名称。
这最常用于针对 Interface 或 Union 类型进行查询,以识别已返回的可能类型的实际类型。
此字段是隐式的,不会出现在任何已定义类型中的字段列表中。
模式内省系统可以从元字段 __schema
和 __type
访问,这些元字段可以从查询操作根类型访问。
__schema: __Schema!
__type(name: String!): __Type
这些字段是隐式的,不会出现在查询操作的根类型的字段列表中。
GraphQL 模式内省系统的模式
type __Schema {
types: [__Type!]!
queryType: __Type!
mutationType: __Type
subscriptionType: __Type
directives: [__Directive!]!
}
type __Type {
kind: __TypeKind!
name: String
description: String
# OBJECT and INTERFACE only
fields(includeDeprecated: Boolean = false): [__Field!]
# OBJECT only
interfaces: [__Type!]
# INTERFACE and UNION only
possibleTypes: [__Type!]
# ENUM only
enumValues(includeDeprecated: Boolean = false): [__EnumValue!]
# INPUT_OBJECT only
inputFields: [__InputValue!]
# NON_NULL and LIST only
ofType: __Type
}
type __Field {
name: String!
description: String
args: [__InputValue!]!
type: __Type!
isDeprecated: Boolean!
deprecationReason: String
}
type __InputValue {
name: String!
description: String
type: __Type!
defaultValue: String
}
type __EnumValue {
name: String!
description: String
isDeprecated: Boolean!
deprecationReason: String
}
enum __TypeKind {
SCALAR
OBJECT
INTERFACE
UNION
ENUM
INPUT_OBJECT
LIST
NON_NULL
}
type __Directive {
name: String!
description: String
locations: [__DirectiveLocation!]!
args: [__InputValue!]!
}
enum __DirectiveLocation {
QUERY
MUTATION
SUBSCRIPTION
FIELD
FRAGMENT_DEFINITION
FRAGMENT_SPREAD
INLINE_FRAGMENT
SCHEMA
SCALAR
OBJECT
FIELD_DEFINITION
ARGUMENT_DEFINITION
INTERFACE
UNION
ENUM
ENUM_VALUE
INPUT_OBJECT
INPUT_FIELD_DEFINITION
}
__Type
是类型内省系统的核心。它表示系统中的标量、接口、对象类型、联合、枚举。
__Type
还表示类型修饰符,用于修改它引用的类型 (ofType: __Type
)。 这就是我们表示列表、非空类型及其组合的方式。
有几种不同的类型种类。在每种种类中,实际上都有不同的有效字段。这些种类在 __TypeKind
枚举中列出。
表示标量类型,例如 Int、String 和 Boolean。标量不能有字段。
GraphQL 类型设计者应在任何标量的 description 字段中描述数据格式和标量强制规则。
字段
kind
必须返回 __TypeKind.SCALAR
。name
必须返回一个 String。description
可以返回一个 String 或 null。对象类型表示字段集合的具体实例。内省类型(例如 __Type
、__Field
等)是对象的示例。
字段
kind
必须返回 __TypeKind.OBJECT
。name
必须返回一个 String。description
可以返回一个 String 或 null。fields
:此类型上可查询的字段集合。includeDeprecated
,默认为 false。如果为 true,则也会返回已弃用的字段。interfaces
:对象实现的接口集合。联合是一种抽象类型,其中未声明公共字段。联合的可能类型在 possibleTypes
中显式列出。类型可以成为联合的一部分,而无需修改该类型。
字段
kind
必须返回 __TypeKind.UNION
。name
必须返回一个 String。description
可以返回一个 String 或 null。possibleTypes
返回可以在此联合中表示的类型列表。它们必须是对象类型。接口是一种抽象类型,其中声明了公共字段。任何实现接口的类型都必须定义所有字段,名称和类型完全匹配。此接口的实现都在 possibleTypes
中显式列出。
字段
kind
必须返回 __TypeKind.INTERFACE
。name
必须返回一个 String。description
可以返回一个 String 或 null。fields
:此接口要求的字段集合。includeDeprecated
,默认为 false。如果为 true,则也会返回已弃用的字段。possibleTypes
返回实现此接口的类型列表。它们必须是对象类型。枚举是特殊的标量,只能有一组定义的值。
字段
kind
必须返回 __TypeKind.ENUM
。name
必须返回一个 String。description
可以返回一个 String 或 null。enumValues
:EnumValue
的列表。必须至少有一个,并且它们必须具有唯一的名称。includeDeprecated
,默认为 false。如果为 true,则也会返回已弃用的枚举值。输入对象是复合类型,用作查询的输入,定义为命名输入值的列表。
例如,输入对象 Point
可以定义为
Example № 89input Point {
x: Int
y: Int
}
字段
kind
必须返回 __TypeKind.INPUT_OBJECT
。name
必须返回一个 String。description
可以返回一个 String 或 null。inputFields
:InputValue
的列表。列表表示 GraphQL 中的值序列。List 类型是一种类型修饰符:它将另一个类型实例包装在 ofType
字段中,该字段定义列表中每个项的类型。
字段
kind
必须返回 __TypeKind.LIST
。ofType
:任何类型。GraphQL 类型是可为空的。值 null 是字段类型的有效响应。
非空类型是一种类型修饰符:它将另一个类型实例包装在 ofType
字段中。非空类型不允许将 null 作为响应,并指示参数和输入对象字段的必需输入。
kind
必须返回 __TypeKind.NON_NULL
。ofType
:除 Non‐null 之外的任何类型。__Field
类型表示 Object 或 Interface 类型中的每个字段。
字段
name
必须返回一个 Stringdescription
可以返回一个 String 或 nullargs
返回一个 __InputValue
列表,表示此字段接受的参数。type
必须返回一个 __Type
,表示此字段返回的值的类型。isDeprecated
返回 true,否则返回 false。deprecationReason
可选地提供此字段被弃用的原因。__InputValue
类型表示字段和指令参数以及输入对象的 inputFields
。
字段
name
必须返回一个 Stringdescription
可以返回一个 String 或 nulltype
必须返回一个 __Type
,表示此输入值期望的类型。defaultValue
可以返回运行时未提供值的情况下,此输入值使用的默认值的 String 编码(使用 GraphQL 语言)。如果此输入值没有默认值,则返回 null。__EnumValue
类型表示枚举的可能值之一。
字段
name
必须返回一个 Stringdescription
可以返回一个 String 或 nullisDeprecated
返回 true,否则返回 false。deprecationReason
可选地提供此字段被弃用的原因。__Directive
类型表示服务器支持的 Directive。
字段
name
必须返回一个 Stringdescription
可以返回一个 String 或 nulllocations
返回一个 __DirectiveLocation
列表,表示此指令可以放置的有效位置。args
返回一个 __InputValue
列表,表示此指令接受的参数。GraphQL 不仅验证请求在语法上是否正确,还确保在给定 GraphQL 模式的上下文中它是明确且无错误的。
无效的请求在技术上仍然是可执行的,并且将始终产生由 Execution 部分中的算法定义的稳定结果,但是相对于包含验证错误的请求,该结果可能是模棱两可、令人惊讶或意外的,因此执行应仅针对有效的请求发生。
通常,验证在执行之前立即在请求的上下文中执行,但是,如果已知完全相同的请求以前已经过验证,则 GraphQL 服务可以执行请求而无需显式验证它。 例如:请求可以在开发期间进行验证(前提是以后不会更改),或者服务可以验证一次请求并记住结果,以避免将来再次验证相同的请求。 任何客户端或开发时工具都应报告验证错误,并且不允许制定或执行已知在给定时间点无效的请求。
类型系统演变
随着 GraphQL 类型系统模式通过添加新类型和新字段随时间演变,先前有效的请求可能后来变得无效。任何可能导致先前有效的请求变为无效的更改都被认为是破坏性更改。 鼓励 GraphQL 服务和模式维护者避免破坏性更改,但是为了对这些破坏性更改更具弹性,复杂的 GraphQL 系统可能仍然允许执行在某些时候已知没有任何验证错误并且此后没有更改的请求。
示例
对于此模式的本节,我们将假设以下类型系统,以便演示示例
Example № 90type Query {
dog: Dog
}
enum DogCommand { SIT, DOWN, HEEL }
type Dog implements Pet {
name: String!
nickname: String
barkVolume: Int
doesKnowCommand(dogCommand: DogCommand!): Boolean!
isHousetrained(atOtherHomes: Boolean): Boolean!
owner: Human
}
interface Sentient {
name: String!
}
interface Pet {
name: String!
}
type Alien implements Sentient {
name: String!
homePlanet: String
}
type Human implements Sentient {
name: String!
}
enum CatCommand { JUMP }
type Cat implements Pet {
name: String!
nickname: String
doesKnowCommand(catCommand: CatCommand!): Boolean!
meowVolume: Int
}
union CatOrDog = Cat | Dog
union DogOrHuman = Dog | Human
union HumanOrAlien = Human | Alien
形式化规范
解释性文本
GraphQL 执行将仅考虑可执行定义 Operation 和 Fragment。 类型系统定义和扩展是不可执行的,并且在执行期间不被考虑。
为避免歧义,包含 TypeSystemDefinition 的文档对于执行无效。
不打算直接执行的 GraphQL 文档可能包含 TypeSystemDefinition。
例如,以下文档对于执行无效,因为原始执行模式可能不知道提供的类型扩展
Counter Example № 91query getDogName {
dog {
name
color
}
}
extend type Dog {
color: String
}
形式化规范
解释性文本
每个命名操作定义在其名称引用时,在文档中必须是唯一的。
例如,以下文档是有效的
Example № 92query getDogName {
dog {
name
}
}
query getOwnerName {
dog {
owner {
name
}
}
}
而此文档是无效的
Counter Example № 93query getName {
dog {
name
}
}
query getName {
dog {
owner {
name
}
}
}
即使每个操作的类型都不同,它也是无效的
Counter Example № 94query dogOperation {
dog {
name
}
}
mutation dogOperation {
mutateDog {
id
}
}
形式化规范
解释性文本
当文档中仅存在一个查询操作时,GraphQL 允许使用简写形式来定义查询操作。
例如,以下文档是有效的
Example № 95{
dog {
name
}
}
而此文档是无效的
Counter Example № 96{
dog {
name
}
}
query getName {
dog {
owner {
name
}
}
}
形式化规范
解释性文本
订阅操作必须恰好有一个根字段。
有效示例
Example № 97subscription sub {
newMessage {
body
sender
}
}
Example № 98subscription sub {
...newMessageFields
}
fragment newMessageFields on Subscription {
newMessage {
body
sender
}
}
无效示例
Counter Example № 99subscription sub {
newMessage {
body
sender
}
disallowedSecondRootField
}
Counter Example № 100subscription sub {
...multipleSubscriptions
}
fragment multipleSubscriptions on Subscription {
newMessage {
body
sender
}
disallowedSecondRootField
}
内省字段也会被计算在内。以下示例也是无效的
Counter Example № 101subscription sub {
newMessage {
body
sender
}
__typename
}
形式化规范
解释性文本
字段选择的目标字段必须在选择集的作用域类型上定义。别名名称没有限制。
例如,以下片段将无法通过验证
Counter Example № 102fragment fieldNotDefined on Dog {
meowVolume
}
fragment aliasedLyingFieldTargetNotDefined on Dog {
barkVolume: kawVolume
}
对于接口,只能对字段进行直接字段选择。具体实现者的字段与给定接口类型选择集的有效性无关。
例如,以下是有效的
Example № 103fragment interfaceFieldSelection on Pet {
name
}
以下是无效的
Counter Example № 104fragment definedOnImplementorsButNotInterface on Pet {
nickname
}
由于联合不定义字段,因此除非使用元字段 __typename,否则不能直接从联合类型选择集中选择字段。来自联合类型选择集的字段只能通过片段间接查询。
例如,以下是有效的
Example № 105fragment inDirectFieldSelectionOnUnion on CatOrDog {
__typename
... on Pet {
name
}
... on Dog {
barkVolume
}
}
但以下是无效的
Counter Example № 106fragment directFieldSelectionOnUnion on CatOrDog {
name
barkVolume
}
形式化规范
解释性文本
如果在执行期间遇到具有相同响应名称的多个字段选择,则要执行的字段和参数以及结果值应是明确的。 因此,对于可能对同一对象都遇到的任何两个字段选择,只有当它们等效时才有效。
在执行期间,具有相同响应名称的字段的同步执行是通过 MergeSelectionSets() 和 CollectFields() 完成的。
对于简单的手写 GraphQL,此规则显然是明显的开发人员错误,但是嵌套片段会使手动检测变得困难。
以下选择正确合并
Example № 107fragment mergeIdenticalFields on Dog {
name
name
}
fragment mergeIdenticalAliasesAndFields on Dog {
otherName: name
otherName: name
}
以下选择无法合并
Counter Example № 108fragment conflictingBecauseAlias on Dog {
name: nickname
name
}
如果参数相同,则相同的参数也会合并。值和变量都可以正确合并。
例如,以下选择正确合并
Example № 109fragment mergeIdenticalFieldsWithIdenticalArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand(dogCommand: SIT)
}
fragment mergeIdenticalFieldsWithIdenticalValues on Dog {
doesKnowCommand(dogCommand: $dogCommand)
doesKnowCommand(dogCommand: $dogCommand)
}
以下选择无法正确合并
Counter Example № 110fragment conflictingArgsOnValues on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand(dogCommand: HEEL)
}
fragment conflictingArgsValueAndVar on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand(dogCommand: $dogCommand)
}
fragment conflictingArgsWithVars on Dog {
doesKnowCommand(dogCommand: $varOne)
doesKnowCommand(dogCommand: $varTwo)
}
fragment differingArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand
}
以下字段不会合并在一起,但是两者都不可能在同一对象上遇到,因此它们是安全的
Example № 111fragment safeDifferingFields on Pet {
... on Dog {
volume: barkVolume
}
... on Cat {
volume: meowVolume
}
}
fragment safeDifferingArgs on Pet {
... on Dog {
doesKnowCommand(dogCommand: SIT)
}
... on Cat {
doesKnowCommand(catCommand: JUMP)
}
}
但是,字段响应必须是可以合并的形状。 例如,标量值不得不同。 在此示例中,someValue
可能是 String
或 Int
Counter Example № 112fragment conflictingDifferingResponses on Pet {
... on Dog {
someValue: nickname
}
... on Cat {
someValue: meowVolume
}
}
形式化规范
解释性文本
永远不允许对标量或枚举进行字段选择,因为它们是任何 GraphQL 查询的叶子节点。
以下是有效的。
Example № 113fragment scalarSelection on Dog {
barkVolume
}
以下是无效的。
Counter Example № 114fragment scalarSelectionsNotAllowedOnInt on Dog {
barkVolume {
sinceWhen
}
}
相反,GraphQL 查询的叶子字段选择必须是标量或枚举类型。 不允许对没有子字段的对象、接口和联合进行叶子选择。
让我们假设对模式的查询根类型进行以下添加
Example № 115extend type Query {
human: Human
pet: Pet
catOrDog: CatOrDog
}
以下示例是无效的
Counter Example № 116query directQueryOnObjectWithoutSubFields {
human
}
query directQueryOnInterfaceWithoutSubFields {
pet
}
query directQueryOnUnionWithoutSubFields {
catOrDog
}
参数提供给字段和指令。 以下验证规则在两种情况下均适用。
形式化规范
解释性文本
提供给字段或指令的每个参数都必须在字段或指令的可能参数集中定义。
例如,以下是有效的
Example № 117fragment argOnRequiredArg on Dog {
doesKnowCommand(dogCommand: SIT)
}
fragment argOnOptional on Dog {
isHousetrained(atOtherHomes: true) @include(if: true)
}
以下是无效的,因为 command
未在 DogCommand
上定义。
Counter Example № 118fragment invalidArgName on Dog {
doesKnowCommand(command: CLEAN_UP_HOUSE)
}
这也是无效的,因为 unless
未在 @include
上定义。
Counter Example № 119fragment invalidArgName on Dog {
isHousetrained(atOtherHomes: true) @include(unless: false)
}
为了探索更复杂的参数示例,让我们将以下内容添加到我们的类型系统中
Example № 120type Arguments {
multipleReqs(x: Int!, y: Int!): Int!
booleanArgField(booleanArg: Boolean): Boolean
floatArgField(floatArg: Float): Float
intArgField(intArg: Int): Int
nonNullBooleanArgField(nonNullBooleanArg: Boolean!): Boolean!
booleanListArgField(booleanListArg: [Boolean]!): [Boolean]
optionalNonNullBooleanArgField(optionalBooleanArg: Boolean! = false): Boolean!
}
extend type Query {
arguments: Arguments
}
参数的顺序无关紧要。因此,以下两个示例都是有效的。
Example № 121fragment multipleArgs on Arguments {
multipleReqs(x: 1, y: 2)
}
fragment multipleArgsReverseOrder on Arguments {
multipleReqs(y: 1, x: 2)
}
字段和指令将参数视为参数名称到值的映射。参数集中具有相同名称的多个参数是模棱两可且无效的。
形式化规范
解释性文本
参数可以是必需的。如果参数类型为非空且没有默认值,则该参数是必需的。 否则,该参数是可选的。
例如,以下是有效的
Example № 122fragment goodBooleanArg on Arguments {
booleanArgField(booleanArg: true)
}
fragment goodNonNullArg on Arguments {
nonNullBooleanArgField(nonNullBooleanArg: true)
}
可以从具有可空参数的字段中省略参数。
因此,以下查询是有效的
Example № 123fragment goodBooleanArgDefault on Arguments {
booleanArgField
}
但这在必需参数上是无效的。
Counter Example № 124fragment missingRequiredArg on Arguments {
nonNullBooleanArgField
}
提供显式值 null 也是无效的,因为必需参数始终具有非空类型。
Counter Example № 125fragment missingRequiredArg on Arguments {
nonNullBooleanArgField(nonNullBooleanArg: null)
}
形式化规范
解释性文本
片段定义在片段展开中按名称引用。为避免歧义,每个片段的名称在文档中必须是唯一的。
内联片段不被视为片段定义,并且不受此验证规则的影响。
例如,以下文档是有效的
Example № 126{
dog {
...fragmentOne
...fragmentTwo
}
}
fragment fragmentOne on Dog {
name
}
fragment fragmentTwo on Dog {
owner {
name
}
}
而此文档是无效的
Counter Example № 127{
dog {
...fragmentOne
}
}
fragment fragmentOne on Dog {
name
}
fragment fragmentOne on Dog {
owner {
name
}
}
形式化规范
解释性文本
片段必须在模式中存在的类型上指定。这适用于命名片段和内联片段。如果它们未在模式中定义,则查询将无法验证。
例如,以下片段是有效的
Example № 128fragment correctType on Dog {
name
}
fragment inlineFragment on Dog {
... on Dog {
name
}
}
fragment inlineFragment2 on Dog {
... @include(if: true) {
name
}
}
以下片段无法验证
Counter Example № 129fragment notOnExistingType on NotInSchema {
name
}
fragment inlineNotExistingType on Dog {
... on NotInSchema {
name
}
}
形式化规范
解释性文本
片段只能在联合类型、接口类型和对象类型上声明。它们在标量类型上无效。它们只能应用于非叶子字段。此规则适用于内联片段和命名片段。
以下片段声明是有效的
Example № 130fragment fragOnObject on Dog {
name
}
fragment fragOnInterface on Pet {
name
}
fragment fragOnUnion on CatOrDog {
... on Dog {
name
}
}
以下片段声明是无效的
Counter Example № 131fragment fragOnScalar on Int {
something
}
fragment inlineFragOnScalar on Dog {
... on Boolean {
somethingElse
}
}
形式化规范
解释性文本
定义的片段必须在文档中使用。
例如,以下是一个无效的文档
Counter Example № 132fragment nameFragment on Dog { # unused
name
}
{
dog {
name
}
}
字段选择也通过将片段相互扩展来确定。目标片段的选择集与引用目标片段的级别的选择集进行并集运算。
形式化规范
解释性文本
命名片段扩展必须引用文档中定义的片段。如果扩展的目标未定义,则会发生验证错误。
Counter Example № 133{
dog {
...undefinedFragment
}
}
形式化规范
DetectCycles(fragmentDefinition, visited) :
解释性文本
片段扩展图不得形成任何循环,包括自身扩展。 否则,操作可能会无限扩展或在底层数据中的循环上无限执行。
这会使导致无限扩展的片段无效
Counter Example № 134{
dog {
...nameFragment
}
}
fragment nameFragment on Dog {
name
...barkVolumeFragment
}
fragment barkVolumeFragment on Dog {
barkVolume
...nameFragment
}
如果以上片段被内联,这将导致无限大
Example № 135{
dog {
name
barkVolume
name
barkVolume
name
barkVolume
name
# forever...
}
}
这也会使在针对循环数据执行时导致无限递归的片段无效
Counter Example № 136{
dog {
...dogFragment
}
}
fragment dogFragment on Dog {
name
owner {
...ownerFragment
}
}
fragment ownerFragment on Dog {
name
pets {
...dogFragment
}
}
形式化规范
解释性文本
片段在类型上声明,并且仅当运行时对象类型与类型条件匹配时才适用。 它们也在父类型的上下文中扩展。 只有当片段的类型条件可以在父类型中应用时,片段扩展才有效。
在对象类型的作用域中,唯一有效的对象类型片段扩展是应用于同一作用域类型的片段。
例如
Example № 137fragment dogFragment on Dog {
... on Dog {
barkVolume
}
}
以下是无效的
Counter Example № 138fragment catInDogFragmentInvalid on Dog {
... on Cat {
meowVolume
}
}
在对象类型的作用域中,如果对象类型实现了接口或属于联合类型的成员,则可以使用联合类型或接口类型扩展。
例如
Example № 139fragment petNameFragment on Pet {
name
}
fragment interfaceWithinObjectFragment on Dog {
...petNameFragment
}
是有效的,因为 Dog 实现了 Pet 接口。
同样地
Example № 140fragment catOrDogNameFragment on CatOrDog {
... on Cat {
meowVolume
}
}
fragment unionWithObjectFragment on Dog {
...catOrDogNameFragment
}
是有效的,因为 Dog 是 CatOrDog 联合类型的成员。 值得注意的是,如果检查 CatOrDogNameFragment 的内容,您可能会注意到永远不会返回有效的结果。 但是,我们不将其指定为无效,因为我们只考虑片段声明,而不是其主体。
联合类型或接口类型扩展可以在对象类型片段的上下文中使用,但前提是对象类型是该接口或联合类型的可能类型之一。
例如,以下片段是有效的
Example № 141fragment petFragment on Pet {
name
... on Dog {
barkVolume
}
}
fragment catOrDogFragment on CatOrDog {
... on Cat {
meowVolume
}
}
petFragment 是有效的,因为 Dog 实现了 Pet 接口。 catOrDogFragment 是有效的,因为 Cat 是 CatOrDog 联合类型的成员。
相比之下,以下片段是无效的
Counter Example № 142fragment sentientFragment on Sentient {
... on Dog {
barkVolume
}
}
fragment humanOrAlienFragment on HumanOrAlien {
... on Cat {
meowVolume
}
}
Dog 没有实现 Sentient 接口,因此 sentientFragment 永远无法返回有意义的结果。 因此,该片段无效。 同样,Cat 不是 HumanOrAlien 联合类型的成员,它也永远无法返回有意义的结果,使其无效。
联合类型或接口类型片段可以在彼此之间使用。 只要存在至少一个对象类型,该对象类型存在于作用域和扩展的可能类型的交集中,则该扩展被认为是有效的。
因此,例如
Example № 143fragment unionWithInterface on Pet {
...dogOrHumanFragment
}
fragment dogOrHumanFragment on DogOrHuman {
... on Dog {
barkVolume
}
}
被认为是有效的,因为 Dog 实现了 Pet 接口并且是 DogOrHuman 联合类型的成员。
然而
Counter Example № 144fragment nonIntersectingInterfaces on Pet {
...sentientFragment
}
fragment sentientFragment on Sentient {
name
}
是无效的,因为不存在同时实现 Pet 和 Sentient 接口的类型。
格式规范
解释性文本
字面量值必须与它们所在位置期望的类型兼容,根据类型系统章节中定义的强制规则。
位置中期望的类型包括为参数提供值定义的类型、为输入对象字段提供值定义的类型,以及为变量定义提供默认值定义的类型。
以下示例是值字面量的有效用法
Example № 145fragment goodBooleanArg on Arguments {
booleanArgField(booleanArg: true)
}
fragment coercedIntIntoFloatArg on Arguments {
# Note: The input coercion rules for Float allow Int literals.
floatArgField(floatArg: 123)
}
query goodComplexDefaultValue($search: ComplexInput = { name: "Fido" }) {
findDog(complex: $search)
}
不可强制转换的值(例如将字符串转换为整数)是无效的。 以下示例是无效的
Counter Example № 146fragment stringIntoInt on Arguments {
intArgField(intArg: "123")
}
query badComplexValue {
findDog(complex: { name: 123 })
}
形式化规范
解释性文本
输入对象值中提供的每个输入字段都必须在输入对象预期类型的可能字段集中定义。
例如,以下示例输入对象是有效的
Example № 147{
findDog(complex: { name: "Fido" })
}
而以下示例输入对象使用了字段“favoriteCookieFlavor”,该字段未在预期类型上定义
Counter Example № 148{
findDog(complex: { favoriteCookieFlavor: "Bacon" })
}
形式化规范
解释性文本
输入对象不得包含多个同名字段,否则会存在歧义,其中包括语法的忽略部分。
例如,以下查询将无法通过验证。
Counter Example № 149{
field(arg: { field: true, field: false })
}
形式化规范
解释性文本
输入对象字段可能是必需的。 就像字段可能具有必需的参数一样,输入对象也可能具有必需的字段。 如果输入字段具有非空类型且没有默认值,则它是必需的。 否则,输入对象字段是可选的。
形式化规范
解释性文本
GraphQL 服务器定义它们支持哪些指令。 对于指令的每次使用,该指令都必须在该服务器上可用。
形式化规范
解释性文本
GraphQL 服务器定义它们支持哪些指令以及在何处支持它们。 对于指令的每次使用,该指令必须在服务器已声明支持的位置使用。
例如,以下查询将无法通过验证,因为 @skip
未提供 QUERY
作为有效位置。
Counter Example № 150query @skip(if: $foo) {
field
}
形式化规范
解释性文本
指令用于描述它们应用的定义上的一些元数据或行为更改。 当使用多个同名指令时,预期的元数据或行为变得不明确,因此每个位置仅允许使用一个指令。
例如,以下查询将无法通过验证,因为 @skip
已对同一字段使用了两次
Counter Example № 151query ($foo: Boolean = true, $bar: Boolean = false) {
field @skip(if: $foo) @skip(if: $bar)
}
但是,以下示例是有效的,因为 @skip
每个位置仅使用一次,尽管在查询中和在同一命名字段上使用了两次
Example № 152query ($foo: Boolean = true, $bar: Boolean = false) {
field @skip(if: $foo) {
subfieldA
}
field @skip(if: $bar) {
subfieldB
}
}
形式化规范
解释性文本
如果任何操作定义了多个同名变量,则它是模糊且无效的。 即使重复变量的类型相同,它也是无效的。
Counter Example № 153query houseTrainedQuery($atOtherHomes: Boolean, $atOtherHomes: Boolean) {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
多个操作定义同名变量是有效的。 如果两个操作引用相同的片段,则实际上可能是必要的
Example № 154query A($atOtherHomes: Boolean) {
...HouseTrainedFragment
}
query B($atOtherHomes: Boolean) {
...HouseTrainedFragment
}
fragment HouseTrainedFragment {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
形式化规范
解释性文本
变量只能是输入类型。 对象、联合类型和接口类型不能用作输入。
对于这些示例,请考虑以下类型系统添加项
Example № 155input ComplexInput { name: String, owner: String }
extend type Query {
findDog(complex: ComplexInput): Dog
booleanList(booleanListArg: [Boolean!]): Boolean
}
以下查询是有效的
Example № 156query takesBoolean($atOtherHomes: Boolean) {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
query takesComplexInput($complexInput: ComplexInput) {
findDog(complex: $complexInput) {
name
}
}
query TakesListOfBooleanBang($booleans: [Boolean!]) {
booleanList(booleanListArg: $booleans)
}
以下查询是无效的
Counter Example № 157query takesCat($cat: Cat) {
# ...
}
query takesDogBang($dog: Dog!) {
# ...
}
query takesListOfPet($pets: [Pet]) {
# ...
}
query takesCatOrDog($catOrDog: CatOrDog) {
# ...
}
形式化规范
解释性文本
变量的作用域是基于每个操作的。 这意味着在操作上下文中使用的任何变量都必须在该操作的顶层定义
例如
Example № 158query variableIsDefined($atOtherHomes: Boolean) {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
是有效的。 $atOtherHomes 由操作定义。
相比之下,以下查询是无效的
Counter Example № 159query variableIsNotDefined {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
$atOtherHomes 未由操作定义。
片段使此规则变得复杂。 操作传递包含的任何片段都可以访问该操作定义的变量。 片段可以出现在多个操作中,因此变量用法必须与所有这些操作中的变量定义相对应。
例如,以下是有效的
Example № 160query variableIsDefinedUsedInSingleFragment($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
因为 isHousetrainedFragment 在操作 variableIsDefinedUsedInSingleFragment 的上下文中使用,并且变量由该操作定义。
另一方面,如果片段包含在未定义引用变量的操作中,则查询无效。
Counter Example № 161query variableIsNotDefinedUsedInSingleFragment {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
这也适用于传递性,因此以下也失败
Counter Example № 162query variableIsNotDefinedUsedInNestedFragment {
dog {
...outerHousetrainedFragment
}
}
fragment outerHousetrainedFragment on Dog {
...isHousetrainedFragment
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
变量必须在使用片段的所有操作中定义。
Example № 163query housetrainedQueryOne($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
query housetrainedQueryTwo($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
但是,以下内容无法通过验证
Counter Example № 164query housetrainedQueryOne($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
query housetrainedQueryTwoNotDefined {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
这是因为 housetrainedQueryTwoNotDefined 未定义变量 $atOtherHomes,但该变量被 isHousetrainedFragment 使用,而 isHousetrainedFragment 包含在该操作中。
形式化规范
解释性文本
由操作定义的所有变量都必须在该操作或该操作传递包含的片段中使用。 未使用的变量会导致验证错误。
例如,以下是无效的
Counter Example № 165query variableUnused($atOtherHomes: Boolean) {
dog {
isHousetrained
}
}
因为 $atOtherHomes 未被引用。
这些规则也适用于传递片段扩展
Example № 166query variableUsedInFragment($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
以上是有效的,因为 $atOtherHomes 在 isHousetrainedFragment 中使用,而 isHousetrainedFragment 被 variableUsedInFragment 包含。
如果该片段没有对 $atOtherHomes 的引用,则它将无效
Counter Example № 167query variableNotUsedWithinFragment($atOtherHomes: Boolean) {
dog {
...isHousetrainedWithoutVariableFragment
}
}
fragment isHousetrainedWithoutVariableFragment on Dog {
isHousetrained
}
文档中的所有操作都必须使用其所有变量。
因此,以下文档无法通过验证。
Counter Example № 168query queryWithUsedVar($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
query queryWithExtraVar($atOtherHomes: Boolean, $extra: Int) {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
此文档无效,因为 queryWithExtraVar 定义了一个多余的变量。
形式化规范
解释性文本
变量用法必须与传递给它们的参数兼容。
当变量在类型完全不匹配的上下文中使用,或者变量中的可空类型传递给非空参数类型时,会发生验证失败。
类型必须匹配
Counter Example № 169query intCannotGoIntoBoolean($intArg: Int) {
arguments {
booleanArgField(booleanArg: $intArg)
}
}
类型为 Int 的 $intArg 不能用作类型为 Boolean 的 booleanArg 的参数。
列表基数也必须相同。 例如,列表不能传递给单数值。
Counter Example № 170query booleanListCannotGoIntoBoolean($booleanListArg: [Boolean]) {
arguments {
booleanArgField(booleanArg: $booleanListArg)
}
}
空值性也必须受到尊重。 一般来说,可空变量不能传递给非空参数。
Counter Example № 171query booleanArgQuery($booleanArg: Boolean) {
arguments {
nonNullBooleanArgField(nonNullBooleanArg: $booleanArg)
}
}
对于列表类型,关于空值性的相同规则适用于外部类型和内部类型。 可空列表不能传递给非空列表,可空值列表不能传递给非空值列表。 以下是有效的
Example № 172query nonNullListToList($nonNullBooleanList: [Boolean]!) {
arguments {
booleanListArgField(booleanListArg: $nonNullBooleanList)
}
}
但是,可空列表不能传递给非空列表
Counter Example № 173query listToNonNullList($booleanList: [Boolean]) {
arguments {
nonNullBooleanListField(nonNullBooleanListArg: $booleanList)
}
}
这将无法通过验证,因为 [T]
不能传递给 [T]!
。 同样,[T]
不能传递给 [T!]
。
当存在默认值时允许可选变量
变量类型兼容性的一个显着例外是允许将具有可空类型的变量定义提供给非空位置,只要该变量或该位置提供默认值即可。
Example № 174query booleanArgQueryWithDefault($booleanArg: Boolean) {
arguments {
optionalNonNullBooleanArgField(optionalBooleanArg: $booleanArg)
}
}
在上面的示例中,允许在提供默认值的非空参数中使用可选变量。
Example № 175query booleanArgQueryWithDefault($booleanArg: Boolean = true) {
arguments {
nonNullBooleanArgField(nonNullBooleanArg: $booleanArg)
}
}
在上面的示例中,变量提供了默认值,并且可以在非空参数中使用。 为了与本规范的早期版本兼容,显式支持此行为。 GraphQL 创作工具可能希望将其报告为警告,并建议将 Boolean
替换为 Boolean!
。
null
仍然可以在运行时提供给这样的变量。 如果为非空参数提供 null
值,则必须产生字段错误。GraphQL 通过执行从请求生成响应。
执行请求由以下几部分信息组成
鉴于这些信息,ExecuteRequest() 的结果会生成响应,该响应将根据下面的“响应”部分进行格式化。
为了执行请求,执行器必须具有已解析的 文档 和要运行的选定操作名称(如果文档定义了多个操作),否则文档应仅包含单个操作。请求的结果由根据以下“执行操作”部分执行此操作的结果确定。
正如“验证”部分所述,只有通过所有验证规则的请求才应执行。如果已知验证错误,则应在响应的“errors”列表中报告这些错误,并且请求必须失败且不执行。
通常,验证是在紧靠执行之前的请求上下文中执行的,但是,如果已知完全相同的请求之前已验证过,则 GraphQL 服务可以在不立即验证请求的情况下执行请求。GraphQL 服务应仅执行在某个时候已知没有任何验证错误,并且此后未更改的请求。
例如:请求可以在开发期间进行验证,前提是它之后没有更改,或者服务可以验证一次请求并记住结果,以避免将来再次验证相同的请求。
如果操作定义了任何变量,则需要使用变量声明类型的输入强制规则来强制这些变量的值。如果在变量值的输入强制期间遇到查询错误,则操作将失败且不执行。
类型系统(如规范的“类型系统”部分所述)必须提供查询根对象类型。如果支持变更或订阅,则还必须分别提供变更或订阅根对象类型。
如果操作是查询,则操作的结果是使用查询根对象类型执行查询的顶层选择集的结果。
在执行查询时,可以提供初始值。
如果操作是变更,则操作的结果是在变更根对象类型上执行变更的顶层选择集的结果。此选择集应按顺序执行。
预期变更操作中的顶层字段会对底层数据系统执行副作用。按顺序执行提供的变更可确保在这些副作用期间避免竞争条件。
如果操作是订阅,则结果是一个名为“响应流”的事件流,其中事件流中的每个事件都是在底层“源流”上为每个新事件执行操作的结果。
执行订阅会在服务器上创建一个持久函数,该函数将底层源流映射到返回的响应流。
作为一个示例,考虑一个聊天应用程序。要订阅发布到聊天室的新消息,客户端发送如下请求
Example № 176subscription NewMessages {
newMessage(roomId: 123) {
sender
text
}
}
当客户端订阅时,每当有新消息发布到 ID 为“123”的聊天室时,将评估“sender”和“text”的选择,并将其发布到客户端,例如
Example № 177{
"data": {
"newMessage": {
"sender": "Hagrid",
"text": "You're a wizard!"
}
}
}
“发布到聊天室的新消息”可以使用“发布-订阅”系统,其中聊天室 ID 是“主题”,每个“发布”都包含发送者和文本。
事件流
事件流表示随时间推移的可观察的离散事件序列。例如,“发布-订阅”系统可能会在“订阅主题”时产生事件流,并且对于该主题的每次“发布”,都会在该事件流上发生一个事件。事件流可能会产生无限的事件序列,也可能在任何时候完成。事件流可能会因错误而完成,或者仅仅是因为不会再发生更多事件。观察者可以在任何时候决定通过取消事件流来停止观察事件流,此后它必须不再从该事件流接收事件。
大规模支持订阅
对于任何 GraphQL 服务来说,支持订阅都是一个重大变化。查询和变更操作是无状态的,允许通过克隆 GraphQL 服务器实例进行扩展。相比之下,订阅是有状态的,需要在订阅的生命周期内维护 GraphQL 文档、变量和其他上下文。
考虑当服务中单台机器发生故障导致状态丢失时,系统的行为。通过拥有单独的专用服务来管理订阅状态和客户端连接,可以提高持久性和可用性。
交付无关
GraphQL 订阅不需要任何特定的序列化格式或传输机制。订阅指定了创建流、该流上每个有效负载的内容以及关闭该流的算法。有意地没有关于消息确认、缓冲、重发请求或任何其他服务质量 (QoS) 详细信息的规范。消息序列化、传输机制和服务质量详细信息应由实现服务选择。
源流表示事件序列,每个事件都将触发与该事件对应的 GraphQL 执行。与字段值解析一样,创建源流的逻辑是特定于应用程序的。
底层源流中的每个事件都触发订阅选择集的执行,并将该事件用作根值。
当客户端不再希望接收订阅的有效负载时,取消订阅会取消响应流。这反过来也可能取消源流。这也是清理订阅使用的任何其他资源的好机会。
要执行选择集,需要知道正在评估的对象值和对象类型,以及它是否必须按顺序执行,或者是否可以并行执行。
首先,选择集被转换为分组字段集;然后,分组字段集中的每个表示字段都会在响应映射中生成一个条目。
错误和 Non‐Null 字段
如果在 ExecuteSelectionSet() 期间,具有 non‐null fieldType 的字段抛出字段错误,则该错误必须传播到整个选择集,如果允许,则解析为 null,或者进一步传播到父字段。
如果发生这种情况,则可以取消任何尚未执行或尚未产生值的同级字段,以避免不必要的工作。
有关此行为的更多信息,请参阅“字段执行”的错误和 Non‐Null 性部分。
通常,执行器可以按其选择的任何顺序(通常是并行)执行分组字段集中的条目。由于除顶层变更字段之外的字段的解析必须始终是无副作用且幂等的,因此执行顺序不得影响结果,因此服务器可以自由地按其认为最佳的任何顺序执行字段条目。
例如,给定以下要正常执行的分组字段集
Example № 178{
birthday {
month
}
address {
street
}
}
有效的 GraphQL 执行器可以按其选择的任何顺序解析这四个字段(当然,birthday
必须在 month
之前解析,address
必须在 street
之前解析)。
执行变更时,最顶层选择集中的选择将按顺序执行,从文本上第一个出现的字段开始。
按顺序执行分组字段集时,执行器必须按照分组字段集中提供的顺序考虑分组字段集中的每个条目。它必须确定结果映射中每个项目的对应条目,直到完成,然后才能继续处理分组字段集中的下一个项目
例如,给定以下要按顺序执行的选择集
Example № 179{
changeBirthday(birthday: $newBirthday) {
month
}
changeAddress(address: $newAddress) {
street
}
}
执行器必须按顺序执行
changeBirthday
运行 ExecuteField(),它在 CompleteValue() 期间将正常执行 { month }
子选择集。changeAddress
运行 ExecuteField(),它在 CompleteValue() 期间将正常执行 { street }
子选择集。作为一个说明性示例,假设我们有一个变更字段 changeTheNumber
,它返回一个包含一个字段 theNumber
的对象。如果我们按顺序执行以下选择集
Example № 180{
first: changeTheNumber(newNumber: 1) {
theNumber
}
second: changeTheNumber(newNumber: 3) {
theNumber
}
third: changeTheNumber(newNumber: 2) {
theNumber
}
}
执行器将按顺序执行以下操作
changeTheNumber(newNumber: 1)
字段first
的 { theNumber }
子选择集changeTheNumber(newNumber: 3)
字段second
的 { theNumber }
子选择集changeTheNumber(newNumber: 2)
字段third
的 { theNumber }
子选择集正确的执行器必须为该选择集生成以下结果
Example № 181{
"first": {
"theNumber": 1
},
"second": {
"theNumber": 3
},
"third": {
"theNumber": 2
}
}
在执行之前,通过调用 CollectFields() 将选择集转换为分组字段集。分组字段集中的每个条目都是共享响应键(别名,如果已定义,否则为字段名称)的字段列表。这确保了通过引用的片段包含的具有相同响应键的所有字段同时执行。
例如,收集此选择集的字段将收集字段 a
的两个实例和字段 b
的一个实例
Example № 182{
a {
subfield1
}
...ExampleFragment
}
fragment ExampleFragment on Query {
a {
subfield2
}
b
}
通过执行维护 CollectFields() 生成的字段组的深度优先搜索顺序,确保字段以稳定且可预测的顺序出现在执行的响应中。
@skip
,则设 skipDirective 为该指令。@include
,则设 includeDirective 为该指令。在选定 objectType 上定义的分组字段集中请求的每个字段都将在响应映射中产生一个条目。字段执行首先强制任何提供的参数值,然后解析字段的值,最后通过递归执行另一个选择集或强制标量值来完成该值。
字段可以包含参数,这些参数提供给底层运行时,以便正确生成值。这些参数由类型系统中的字段定义为具有特定的输入类型。
在查询中的每个参数位置,可以是字面量 Value,也可以是在运行时提供的 Variable。
虽然几乎所有 GraphQL 的执行都可以进行通用描述,但最终暴露 GraphQL 接口的内部系统必须提供值。这通过 ResolveFieldValue 公开,它为一个给定类型的字段和一个实际值生成一个值。
例如,这可能会接受 objectType Person
,field "soulMate",以及代表约翰·列侬的 objectValue。它应该产生代表小野洋子的值。
在解析字段的值之后,通过确保其符合预期的返回类型来完成它。如果返回类型是另一个 Object 类型,则字段执行过程继续递归。
解析抽象类型
当完成一个具有抽象返回类型的字段时,即 Interface 或 Union 返回类型,首先必须将抽象类型解析为相关的 Object 类型。此确定由内部系统使用任何适当的方式进行。
合并选择集
当并行执行多个同名字段时,它们的选择集在完成值时合并在一起,以便继续执行子选择集。
一个示例查询,说明具有相同名称和子选择的并行字段。
Example № 183{
me {
firstName
}
me {
lastName
}
}
在解析 me
的值之后,选择集合并在一起,以便可以为一个值解析 firstName
和 lastName
。
如果在解析字段时抛出错误,则应将其视为字段返回 null,并且必须将错误添加到响应中的 "errors" 列表中。
如果解析字段的结果是 null(要么是因为解析字段的函数返回 null,要么是因为发生了错误),并且该字段是 Non-Null
类型,则会抛出一个字段错误。该错误必须添加到响应中的 "errors" 列表中。
如果字段由于已添加到响应中的 "errors" 列表中的错误而返回 null,则 "errors" 列表不得进一步受到影响。也就是说,每个字段的错误列表中应只添加一个错误。
由于 Non-Null
类型字段不能为 null,因此字段错误会传播到父字段以进行处理。如果父字段可以为 null,则它解析为 null,否则,如果它是 Non-Null
类型,则字段错误会进一步传播到其父字段。
如果 List
类型包装了 Non-Null
类型,并且该列表中的一个元素解析为 null,则整个列表必须解析为 null。如果 List
类型也包装在 Non-Null
中,则字段错误将继续向上传播。
如果从请求的根到字段错误的源的所有字段都返回 Non-Null
类型,则响应中的 "data" 条目应为 null。
当 GraphQL 服务器收到请求时,它必须返回一个格式良好的响应。服务器的响应描述了成功执行请求操作的结果,并描述了请求期间遇到的任何错误。
在字段错误发生在被替换为 null 的字段上的情况下,响应可能同时包含部分响应和遇到的错误。
GraphQL 操作的响应必须是一个映射。
如果操作遇到任何错误,则响应映射必须包含一个键为 errors
的条目。此条目的值在“错误”部分中描述。如果操作完成时没有遇到任何错误,则不得存在此条目。
如果操作包括执行,则响应映射必须包含一个键为 data
的条目。此条目的值在“数据”部分中描述。如果操作在执行之前失败,由于语法错误、缺少信息或验证错误,则不得存在此条目。
响应映射也可能包含一个键为 extensions
的条目。如果设置了此条目,则其值必须是一个映射。此条目为实现者保留,用于以他们认为合适的方式扩展协议,因此对其内容没有额外的限制。
为了确保协议的未来更改不会破坏现有的服务器和客户端,顶级响应映射不得包含除上述三个之外的任何条目。
errors
出现在响应中时,将其序列化时最好首先出现,以便在调试期间更清楚地了解响应中是否存在错误。响应中的 data
条目将是请求操作的执行结果。如果操作是查询,则此输出将是模式的查询根类型的对象;如果操作是 mutation,则此输出将是模式的 mutation 根类型的对象。
如果在执行开始之前遇到错误,则 data
条目不应出现在结果中。
如果在执行期间遇到阻止有效响应的错误,则响应中的 data
条目应为 null
。
响应中的 errors
条目是一个非空错误列表,其中每个错误都是一个映射。
如果在请求的操作期间未遇到任何错误,则 errors
条目不应出现在结果中。
如果响应中的 data
条目不存在,则响应中的 errors
条目不得为空。它必须至少包含一个错误。它包含的错误应指示为什么无法返回数据。
如果响应中的 data
条目存在(包括如果它是值 null),则响应中的 errors
条目可能包含执行期间发生的任何错误。如果执行期间发生错误,则应包含这些错误。
错误结果格式
每个错误都必须包含一个键为 message
的条目,其中包含错误的字符串描述,旨在为开发人员提供指导,以理解和纠正错误。
如果错误可以与请求的 GraphQL 文档中的特定点相关联,则它应包含一个键为 locations
的条目,其中包含位置列表,其中每个位置都是一个具有键 line
和 column
的映射,两者都是从 1
开始的正数,用于描述关联的语法元素的开始位置。
如果错误可以与 GraphQL 结果中的特定字段相关联,则它必须包含一个键为 path
的条目,该条目详细说明了遇到错误的响应字段的路径。这允许客户端识别 null
结果是故意的还是由运行时错误引起的。
此字段应是一个路径段列表,从响应的根开始,到与错误关联的字段结束。表示字段的路径段应为字符串,表示列表索引的路径段应为 0 索引整数。如果错误发生在别名字段中,则错误的路径应使用别名,因为它表示响应中的路径,而不是查询中的路径。
例如,如果在以下查询中获取其中一个朋友的名字失败
Example № 184{
hero(episode: $episode) {
name
heroFriends: friends {
id
name
}
}
}
响应可能如下所示
Example № 185{
"errors": [
{
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [ { "line": 6, "column": 7 } ],
"path": [ "hero", "heroFriends", 1, "name" ]
}
],
"data": {
"hero": {
"name": "R2-D2",
"heroFriends": [
{
"id": "1000",
"name": "Luke Skywalker"
},
{
"id": "1002",
"name": null
},
{
"id": "1003",
"name": "Leia Organa"
}
]
}
}
}
如果遇到错误的字段被声明为 Non-Null
,则 null
结果将冒泡到下一个可空字段。在这种情况下,错误的 path
应包含到发生错误的结果字段的完整路径,即使该字段未出现在响应中。
例如,如果上面示例中的 name
字段在模式中声明了 Non-Null
返回类型,则结果将看起来不同,但报告的错误将是相同的
Example № 186{
"errors": [
{
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [ { "line": 6, "column": 7 } ],
"path": [ "hero", "heroFriends", 1, "name" ]
}
],
"data": {
"hero": {
"name": "R2-D2",
"heroFriends": [
{
"id": "1000",
"name": "Luke Skywalker"
},
null,
{
"id": "1003",
"name": "Leia Organa"
}
]
}
}
}
GraphQL 服务可能会为错误提供一个额外的键为 extensions
的条目。如果设置了此条目,则其值必须是一个映射。此条目为实现者保留,用于向错误添加额外信息,但对其内容没有额外的限制。
Example № 187{
"errors": [
{
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [ { "line": 6, "column": 7 } ],
"path": [ "hero", "heroFriends", 1, "name" ],
"extensions": {
"code": "CAN_NOT_FETCH_BY_ID",
"timestamp": "Fri Feb 9 14:33:09 UTC 2018"
}
}
]
}
GraphQL 服务不应为错误格式提供任何额外的条目,因为它们可能会与此规范未来版本中可能添加的额外条目冲突。
extensions
条目。虽然未指定的条目不是违规行为,但仍然不鼓励使用它们。Counter Example № 188{
"errors": [
{
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [ { "line": 6, "column": 7 } ],
"path": [ "hero", "heroFriends", 1, "name" ],
"code": "CAN_NOT_FETCH_BY_ID",
"timestamp": "Fri Feb 9 14:33:09 UTC 2018"
}
]
}
GraphQL 不要求特定的序列化格式。但是,客户端应使用支持 GraphQL 响应中主要原语的序列化格式。特别是,序列化格式必须至少支持以下四个原语的表示
序列化格式还应支持以下原语,每个原语代表一个常见的 GraphQL 标量类型,但是,如果任何原语不直接支持,则可以使用字符串或更简单的原语作为替代
这并非旨在详尽列出序列化格式可以编码的内容。例如,表示日期、时间、URI 或具有不同精度的数字的自定义标量可以使用给定序列化格式可能支持的任何相关格式表示。
JSON 是 GraphQL 最常见的序列化格式。尽管如上所述,GraphQL 不要求特定的序列化格式。
当使用 JSON 作为 GraphQL 响应的序列化时,应使用以下 JSON 值来编码相关的 GraphQL 值
GraphQL 值 | JSON 值 |
---|---|
映射 | 对象 |
列表 | 数组 |
Null | null |
字符串 | 字符串 |
布尔值 | true 或 false |
整数 | 数字 |
浮点数 | 数字 |
枚举值 | 字符串 |
由于评估选择集的结果是有序的,因此序列化的结果映射应通过以与查询执行定义的请求字段相同的顺序写入映射条目来保留此顺序。生成一个序列化响应,其中字段以它们在请求中出现的相同顺序表示,可以提高调试期间的人工可读性,并且如果可以预期属性的顺序,则可以更有效地解析响应。
表示有序映射的序列化格式应保留由执行部分中的 CollectFields() 定义的请求字段的顺序。仅表示无序映射但顺序仍然隐含在序列化的文本顺序中的序列化格式(例如 JSON)应在文本上保留请求字段的顺序。
例如,如果请求是 { name, age }
,则以 JSON 响应的 GraphQL 服务应使用 { "name": "Mark", "age": 30 }
响应,而不应使用 { "age": 30, "name": "Mark" }
响应。
虽然 JSON 对象被指定为 键值对的无序集合,但这些对以有序的方式表示。换句话说,虽然 JSON 字符串 { "name": "Mark", "age": 30 }
和 { "age": 30, "name": "Mark" }
编码相同的值,但它们也具有可观察到的不同属性顺序。
本规范文档包含许多用于描述技术概念(例如语言语法和语义以及运行时算法)的符号约定。
本附录旨在更详细地解释这些符号,以避免歧义。
上下文无关文法由许多产生式组成。每个产生式都有一个称为“非终结符”的抽象符号作为其左侧,以及零个或多个可能的非终结符序列和/或终结符字符作为其右侧。
从单个目标非终结符开始,上下文无关文法描述了一种语言:可以通过重复将目标序列中的任何非终结符替换为其定义的一个序列,直到所有非终结符都被终结符字符替换为止,来描述的可能字符序列的集合。
终结符在本文档中以等宽字体以两种形式表示:特定的 Unicode 字符或 Unicode 字符序列(例如 = 或 terminal),以及由正则表达式定义的 Unicode 字符模式(例如 /[0-9]+/)。
非终结符产生式规则在本文档中使用以下表示法表示具有单个定义的非终结符
同时使用以下表示法表示具有多个定义的产生式
定义可以引用自身,这描述了重复序列,例如
GraphQL 语言在句法文法中定义,其中终结符是标记。标记在词法文法中定义,词法文法匹配源字符的模式。解析源 Unicode 字符序列的结果生成 GraphQL AST。
词法文法产生式通过终结符 Unicode 字符的模式来描述非终结符“标记”。在词法文法产生式中的任何终结符 Unicode 字符之间,不得出现“空白”或其他忽略的字符。词法文法产生式以双冒号 ::
定义区分。
句法文法产生式通过终结符标记的模式来描述非终结符“规则”。空白和其他忽略的字符可以出现在任何终结符标记之前或之后。句法文法产生式以单冒号 :
定义区分。
本规范使用一些额外的符号来描述常见模式,例如可选或重复模式,或非终结符定义的参数化变更。本节解释这些简写符号及其在上下文无关文法中的扩展定义。
约束
文法产生式可以使用短语“but not”,然后指示要排除的扩展,来指定不允许某些扩展。
例如,产生式
表示非终结符 SafeName 可以被任何可以替换 Name 的字符序列替换,前提是相同的字符序列不能替换 SevenCarlinWords。
文法也可以在“but not”之后列出多个由“or”分隔的限制。
例如
可选性和列表
下标后缀 “Symbolopt” 是两种可能序列的简写,一种包含该符号,另一种排除该符号。
例如
是以下内容的简写
下标后缀 “Symbollist” 是一个或多个该符号的列表的简写。
例如
是以下内容的简写
参数化文法产生式
花括号中的符号定义下标后缀参数 “SymbolParam” 是两个符号定义的简写,一个附加了该参数名称,另一个没有。符号上的相同下标后缀是该定义的变体的简写。如果参数以“?”开头,则在具有相同参数的符号定义中使用该形式的符号。当分别以“[+Param]”和“[~Param]”为前缀时,可以有条件地包含或排除某些可能的序列。
例如
是以下内容的简写
本规范以算法步骤列表的形式描述了许多文法产生式的语义值。
例如,这描述了解析器应如何解释字符串文字
本规范描述了静态和运行时语义使用的一些算法,它们以类似于函数的语法定义,包括算法的名称和它接受的参数,以及要按顺序列出的算法步骤列表。每个步骤都可以建立对其他值的引用,检查各种条件,调用其他算法,并最终返回一个值,该值表示为提供的参数的算法结果。
例如,以下示例描述了一个名为 Fibonacci 的算法,它接受一个参数 number。该算法的步骤生成斐波那契数列中的下一个数字
! | $ | ( | ) | ... | : | = | @ | [ | ] | { | | | } |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
e | E |
+ | - |
" | \ | / | b | f | n | r | t |
query | mutation | subscription |
true | false |
QUERY |
MUTATION |
SUBSCRIPTION |
FIELD |
FRAGMENT_DEFINITION |
FRAGMENT_SPREAD |
INLINE_FRAGMENT |
SCHEMA |
SCALAR |
OBJECT |
FIELD_DEFINITION |
ARGUMENT_DEFINITION |
INTERFACE |
UNION |
ENUM |
ENUM_VALUE |
INPUT_OBJECT |
INPUT_FIELD_DEFINITION |