工作草案 – 2016年10月
简介
这是 GraphQL 的 RFC 规范草案,GraphQL 是一种查询语言,由 Facebook 于 2012 年创建,用于描述客户端-服务器应用程序的数据模型的功能和需求。此标准的制定工作始于 2015 年。GraphQL 是一种新兴且不断发展的语言,尚未完成。在本规范的未来版本中将继续进行重大改进。
版权声明
版权所有 (c) 2015‐2016, Facebook, Inc. 保留所有权利。
在满足以下条件的情况下,允许以源代码和二进制形式重新分发和使用,无论是否进行修改:
本软件由版权所有者和贡献者“按原样”提供,并且不承担任何明示或暗示的保证,包括但不限于对适销性和特定用途适用性的暗示保证。在任何情况下,版权所有者或贡献者均不对任何直接、间接、偶然、特殊、惩戒性或后果性损害(包括但不限于采购替代商品或服务;使用、数据或利润损失;或业务中断),无论因何种原因引起以及基于何种责任理论(无论是合同、严格责任还是侵权行为(包括疏忽或其他))承担责任,即使已被告知可能发生此类损害。
GraphQL 是一种查询语言,旨在通过提供直观且灵活的语法和系统来构建客户端应用程序,以描述其数据需求和交互。
例如,以下 GraphQL 请求将从 Facebook 的 GraphQL 实现中接收 id 为 4 的用户的姓名。
{
user(id: 4) {
name
}
}
这将产生结果数据(JSON 格式)
{
"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 查询文档包含操作时,服务器才能执行该文档。但是,不包含操作的文档仍然可以被解析和验证,以允许客户端表示跨多个文档的单个请求。
如果文档仅包含一个操作,则该操作可能是未命名的或以简写形式表示,简写形式省略了 query 关键字和操作名称。否则,如果 GraphQL 查询文档包含多个操作,则每个操作都必须命名。当向 GraphQL 服务提交包含多个操作的查询文档时,还必须提供要执行的所需操作的名称。
query | mutation |
GraphQL 建模了两种类型的操作
每个操作都由可选的操作名称和选择集表示。
例如,此 mutation 操作可能会“点赞”一个故事,然后检索新的点赞数
mutation {
likeStory(storyID: 12345) {
story {
likeCount
}
}
}
查询简写
如果文档仅包含一个 query 操作,并且该 query 操作未定义任何变量且不包含任何指令,则该操作可以用简写形式表示,该简写形式省略了 query 关键字和 query 名称。
例如,此未命名的 query 操作是通过 query 简写编写的。
{
field
}
操作选择它需要的信息集,并将准确接收该信息,不多也不少,从而避免过度获取和获取不足数据。
{
id
firstName
lastName
}
在此查询中,id
、firstName
和 lastName
字段形成一个选择集。选择集也可能包含片段引用。
选择集主要由字段组成。字段描述了可在选择集中请求的一个离散信息片段。
某些字段描述复杂数据或与其他数据的关系。为了进一步探索此数据,字段本身可以包含一个选择集,从而允许深度嵌套的请求。所有 GraphQL 操作都必须将其选择指定到返回标量值的字段,以确保明确形状的响应。
例如,此操作选择复杂数据和关系的字段,一直到标量值。
{
me {
id
firstName
lastName
birthday {
month
day
}
friends {
name
}
}
}
操作的顶层选择集中的字段通常表示您的应用程序及其当前查看者全局可访问的某些信息。这些顶级字段的一些典型示例包括对当前登录查看者的引用,或访问由唯一标识符引用的某些类型的数据。
# `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
的个人资料图片
{
user(id: 4) {
id
name
profilePic(size: 100)
}
}
给定字段可以存在多个参数
{
user(id: 4) {
id
name
profilePic(width: 100, height: 50)
}
}
参数是无序的
可以以任何语法顺序提供参数,并保持相同的语义意义。
以下两个查询在语义上是相同的
{
picture(width: 200, height: 100)
}
{
picture(height: 100, width: 200)
}
默认情况下,响应对象中的键将使用查询的字段名称。但是,您可以通过指定别名来定义不同的名称。
在此示例中,我们可以获取两个不同大小的个人资料图片,并确保结果对象不会有重复的键
{
user(id: 4) {
id
name
smallPic: profilePic(size: 64)
bigPic: profilePic(size: 1024)
}
}
这将返回结果
{
"user": {
"id": 4,
"name": "Mark Zuckerberg",
"smallPic": "https://cdn.site.io/pic-4-64.jpg",
"bigPic": "https://cdn.site.io/pic-4-1024.jpg"
}
}
由于查询的顶层是一个字段,因此也可以为其指定别名
{
zuck: user(id: 4) {
id
name
}
}
返回结果
{
"zuck": {
"id": 4,
"name": "Mark Zuckerberg"
}
}
如果提供了别名,则字段的响应键是其别名,否则是字段的名称。
片段是 GraphQL 中主要的组合单元。
片段允许重用字段的常见重复选择,从而减少文档中的重复文本。当针对接口或联合进行查询时,内联片段可以直接在选择中使用,以根据类型条件进行条件化。
例如,如果我们想获取关于共同好友的一些共同信息以及某些用户的好友
query noFragments {
user(id: 4) {
friends(first: 10) {
id
name
profilePic(size: 50)
}
mutualFriends(first: 10) {
id
name
profilePic(size: 50)
}
}
}
重复字段可以提取到片段中,并由父片段或查询组合。
query withFragments {
user(id: 4) {
friends(first: 10) {
...friendFields
}
mutualFriends(first: 10) {
...friendFields
}
}
}
fragment friendFields on User {
id
name
profilePic(size: 50)
}
片段通过使用展开运算符 (...
) 来使用。片段选择的所有字段都将添加到与片段调用位于同一级别的查询字段选择中。这通过多个级别的片段展开发生。
例如
query 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 数据模型上的查询中
query 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
将不存在。
{
"profiles": [
{
"handle": "zuck",
"friends": { "count" : 1234 }
},
{
"handle": "cocacola",
"likers": { "count" : 90234512 }
}
]
}
片段可以在选择集中内联定义。这样做是为了根据其运行时类型有条件地包含字段。标准片段包含的此功能已在 query FragmentTyping
示例中演示。我们可以使用内联片段完成相同的事情。
query inlineFragmentTyping {
profiles(handles: ["zuck", "cocacola"]) {
handle
... on User {
friends {
count
}
}
... on Page {
likers {
count
}
}
}
}
内联片段也可以用于将指令应用于一组字段。如果省略了 TypeCondition,则内联片段被视为与封闭上下文的类型相同。
query inlineFragmentNoType($expandedInfo: Boolean) {
user(handle: "zuck") {
id
name
... @include(if: $expandedInfo) {
firstName
lastName
birthday
}
}
}
字段和指令参数接受各种字面量原始类型的输入值;输入值可以是标量、枚举值、列表或输入对象。
如果未定义为常量(例如,在 DefaultValue 中),则输入值可以指定为变量。列表和输入对象也可能包含变量(除非定义为常量)。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
整数以不带小数点或指数的形式指定(例如,1
)。
+ | - |
浮点数包括小数点(例如,1.0
)或指数(例如,1e50
)或两者(例如,6.0221413e23
)。
true | false |
关键字 true
和 false
表示两个布尔值。
" | \ | / | b | f | n | r | t |
字符串是以双引号 ("
) 包裹的字符序列。(例如,"Hello World"
)。空白字符和其他原本被忽略的字符在字符串值中很重要。
语义
转义字符 | 代码单元值 | 字符名称 |
---|---|---|
" | U+0022 | 双引号 |
\ | U+005C | 反斜线 |
/ | U+002F | 斜线 |
b | U+0008 | 退格 |
f | U+000C | 换页 |
n | U+000A | 换行符 (新行) |
r | U+000D | 回车符 |
t | U+0009 | 水平制表符 |
空值用关键字 null 表示。
GraphQL 有两种语义上不同的方式来表示值的缺失
例如,以下两个字段调用类似,但并不相同
{
field(arg: null)
field
}
第一个显式地为参数 “arg” 提供了 null,而第二个隐式地没有为参数 “arg” 提供值。这两种形式的解释可能不同。例如,一个 mutation 可能表示删除一个字段,而另一个可能表示不更改字段。两种形式都不能用于期望 Non-Null 类型的输入。
枚举值表示为不带引号的名称(例如 MOBILE_WEB
)。建议枚举值使用“全大写”。枚举值仅在精确的枚举类型已知的情况下使用。因此,在字面值中无需提供枚举类型名称。
列表是用方括号 [ ]
包裹的有序值序列。列表字面值的值可以是任何值字面值或变量(例如 [1, 2, 3]
)。
逗号在整个 GraphQL 中是可选的,因此允许尾随逗号,重复逗号不表示缺少值。
语义
输入对象字面值是用大括号 { }
包裹的键值输入值的无序列表。对象字面值的值可以是任何输入值字面值或变量(例如 { name: "Hello world", score: 1.0 }
)。我们将输入对象的字面表示形式称为“对象字面值”。
输入对象字段是无序的
输入对象字段可以以任何语法顺序提供,并保持相同的语义含义。
以下两个查询在语义上是相同的
{
nearestThing(location: { lon: 12.43, lat: -53.211 })
}
{
nearestThing(location: { lat: -53.211, lon: 12.43 })
}
语义
GraphQL 查询可以使用变量进行参数化,从而最大限度地提高查询重用率,并避免客户端在运行时进行代价高昂的字符串构建。
如果未定义为常量(例如,在 DefaultValue 中),则可以为输入值提供 Variable。
变量必须在操作的顶部定义,并在该操作的整个执行过程中都在作用域内。
在此示例中,我们希望根据特定设备的大小获取个人资料图片大小
query getZuckProfile($devicePicSize: Int) {
user(id: 4) {
id
name
profilePic(size: $devicePicSize)
}
}
这些变量的值与请求一起提供给 GraphQL 服务,以便在执行期间可以替换它们。如果为变量的值提供 JSON,我们可以运行此查询并请求宽度为 60
的 profilePic
{
"devicePicSize": 60
}
片段中的变量使用
查询变量可以在片段中使用。查询变量在给定的操作中具有全局作用域,因此在片段中使用的变量必须在任何传递使用该片段的顶层操作中声明。如果变量在片段中被引用,并且被不定义该变量的操作包含,则该操作无法执行。
GraphQL 描述了查询变量期望的数据类型。输入类型可以是另一个输入类型的列表,或任何其他输入类型的非空变体。
语义
指令提供了一种在 GraphQL 文档中描述替代运行时执行和类型验证行为的方法。
在某些情况下,您需要提供选项来更改 GraphQL 的执行行为,而字段参数不足以满足需求,例如有条件地包含或跳过字段。指令通过向执行器描述附加信息来提供此功能。
指令有一个名称以及一个参数列表,该列表可以接受任何输入类型的值。
指令可用于描述字段、片段和操作的附加信息。
随着未来版本的 GraphQL 采用新的可配置执行能力,它们可能会通过指令公开。
GraphQL 类型系统描述了 GraphQL 服务器的功能,并用于确定查询是否有效。类型系统还描述了查询变量的输入类型,以确定运行时提供的值是否有效。
GraphQL 服务器的功能被称为该服务器的“schema”(模式)。模式根据其支持的类型和指令来定义。
给定的 GraphQL 模式本身必须在内部有效。本节描述了此验证过程的相关规则。
GraphQL 模式由每种操作类型的根类型表示:查询和 mutation;这决定了这些操作在类型系统中开始的位置。
GraphQL 模式中的所有类型都必须具有唯一的名称。任何两个提供的类型都不能具有相同的名称。任何提供的类型都不能具有与任何内置类型(包括标量和内省类型)冲突的名称。
GraphQL 模式中的所有指令都必须具有唯一的名称。指令和类型可以共享相同的名称,因为它们之间没有歧义。
任何 GraphQL Schema 的基本单元都是类型。GraphQL 中有八种类型的类型。
最基本的类型是 Scalar
(标量)。标量表示原始值,例如字符串或整数。通常,标量字段的可能响应是可枚举的。GraphQL 在这些情况下提供 Enum
(枚举)类型,其中类型指定了有效响应的空间。
标量和枚举构成响应树中的叶子;中间级别是 Object
(对象)类型,它定义了一组字段,其中每个字段都是系统中的另一种类型,从而允许定义任意类型层次结构。
GraphQL 支持两种抽象类型:接口和联合。
Interface
(接口)定义了字段列表;实现该接口的 Object
类型保证实现这些字段。每当类型系统声明它将返回一个接口时,它将返回一个有效的实现类型。
Union
(联合)定义了可能的类型列表;与接口类似,每当类型系统声明将返回联合时,将返回其中一种可能的类型。
到目前为止,所有类型都被假定为既可为空又为单数:例如,标量字符串返回 null 或单数字符串。类型系统可能希望定义它返回其他类型的列表;为此提供了 List
(列表)类型,它包装了另一种类型。类似地,Non-Null
(非空)类型包装了另一种类型,并表示结果永远不会为 null。这两种类型被称为“包装类型”;非包装类型被称为“基本类型”。包装类型具有一个底层的“基本类型”,通过不断地解包类型直到找到基本类型来找到。
最后,通常将复杂结构作为 GraphQL 查询的输入很有用;Input Object
(输入对象)类型允许模式精确定义客户端在这些查询中期望的数据。
顾名思义,标量表示 GraphQL 中的原始值。GraphQL 响应采用分层树的形式;这些树上的叶子是 GraphQL 标量。
所有 GraphQL 标量都可以表示为字符串,尽管根据使用的响应格式,对于给定的标量类型可能存在更合适的原始类型,并且服务器应在适当的时候使用这些类型。
GraphQL 提供了许多内置标量,但类型系统可以添加具有语义含义的附加标量。例如,GraphQL 系统可以定义一个名为 Time
的标量,它虽然序列化为字符串,但承诺符合 ISO-8601。当查询 Time
类型的字段时,您可以依赖于使用 ISO-8601 解析器解析结果并使用客户端特定的时间原始类型的功能。另一个可能有用的自定义标量的示例是 Url
,它序列化为字符串,但服务器保证它是有效的 URL。
服务器可以从其模式中省略任何内置标量,例如,如果模式不引用浮点数,则它将不包含 Float
类型。但是,如果模式包含名称与此处描述的类型之一相同的类型,则它必须遵守所描述的行为。例如,服务器不得包含名为 Int
的类型并将其用于表示 128 位数字或国际化信息。
结果强制转换
GraphQL 服务器在准备给定标量类型的字段时,必须维护标量类型描述的约定,或者通过强制转换值或产生错误。
例如,GraphQL 服务器可能正在准备一个标量类型为 Int
的字段,并遇到一个浮点数。由于服务器不得通过产生非整数来破坏约定,因此服务器应截断小数部分,仅产生整数值。如果服务器遇到布尔值 true
,则应返回 1
。如果服务器遇到字符串,则可能会尝试解析字符串以获取十进制整数值。如果服务器遇到一些无法合理强制转换为 Int
的值,则必须引发字段错误。
由于客户端无法观察到此强制转换行为,因此强制转换的具体规则留给实现。唯一的要求是服务器必须产生符合预期标量类型的值。
输入强制转换
如果 GraphQL 服务器期望标量类型作为参数的输入,则强制转换是可观察的,并且规则必须明确定义。如果输入值与强制转换规则不匹配,则必须引发查询错误。
GraphQL 具有不同的常量字面值来表示整数和浮点输入值,并且强制转换规则可能因遇到的输入值类型而异。GraphQL 可以通过查询变量进行参数化,查询变量的值通常在通过 HTTP 等传输发送时进行序列化。由于某些常见的序列化(例如 JSON)不区分整数值和浮点值,因此如果它们具有空的小数部分(例如 1.0
),则将其解释为整数输入值,否则将其解释为浮点输入值。
对于以下所有类型,除了 Non-Null 之外,如果提供了显式值 null,则输入强制转换的结果为 null。
内置标量
GraphQL 提供了一组基本的、定义明确的标量类型。GraphQL 服务器应支持所有这些类型,并且提供这些名称的类型的 GraphQL 服务器必须遵守以下描述的行为。
Int 标量类型表示有符号 32 位数字非小数数值。支持 32 位整数或数字类型的响应格式应使用该类型来表示此标量。
结果强制转换
GraphQL 服务器应在可能的情况下将非 int 原始值强制转换为 Int,否则它们必须引发字段错误。这方面的示例可能包括为浮点数 1.0
返回 1
,或为字符串 "2"
返回 2
。
输入强制转换
当预期作为输入类型时,仅接受整数输入值。所有其他输入值,包括包含数字内容的字符串,都必须引发查询错误,指示类型不正确。如果整数输入值表示的值小于 -231 或大于或等于 231,则应引发查询错误。
Float 标量类型表示由 IEEE 754 指定的有符号双精度小数值。支持适当的双精度数字类型的响应格式应使用该类型来表示此标量。
结果强制转换
GraphQL 服务器应在可能的情况下将非浮点原始值强制转换为 Float,否则它们必须引发字段错误。这方面的示例可能包括为整数 1
返回 1.0
,或为字符串 "2"
返回 2.0
。
输入强制转换
当预期作为输入类型时,整数和浮点输入值都被接受。整数输入值通过添加空的小数部分强制转换为 Float,例如,整数输入值 1
强制转换为 1.0
。所有其他输入值,包括包含数字内容的字符串,都必须引发查询错误,指示类型不正确。如果整数输入值表示的值无法用 IEEE 754 表示,则应引发查询错误。
String 标量类型表示文本数据,表示为 UTF-8 字符序列。String 类型最常用于 GraphQL 来表示自由形式的人类可读文本。所有响应格式都必须支持字符串表示形式,并且此处必须使用该表示形式。
结果强制转换
GraphQL 服务器应在可能的情况下将非字符串原始值强制转换为 String,否则它们必须引发字段错误。这方面的示例可能包括为布尔值 true 返回字符串 "true"
,或为整数 1
返回字符串 "1"
。
输入强制转换
当预期作为输入类型时,仅接受有效的 UTF-8 字符串输入值。所有其他输入值都必须引发查询错误,指示类型不正确。
Boolean 标量类型表示 true
或 false
。响应格式应使用内置布尔类型(如果支持);否则,它们应使用整数 1
和 0
的表示形式。
结果强制转换
GraphQL 服务器应在可能的情况下将非布尔原始值强制转换为 Boolean,否则它们必须引发字段错误。这方面的示例可能包括为任何非零数字返回 true
。
输入强制转换
当预期作为输入类型时,仅接受布尔输入值。所有其他输入值都必须引发查询错误,指示类型不正确。
ID 标量类型表示唯一标识符,通常用于重新获取对象或作为缓存的键。ID 类型的序列化方式与 String
相同;但是,它不旨在供人类阅读。虽然它通常是数字,但它应始终序列化为 String
。
结果强制转换
GraphQL 与 ID 格式无关,并且序列化为字符串以确保 ID 可能表示的许多格式之间的一致性,从小的自增数字到大的 128 位随机数,再到 base64 编码的值,或 GUID 等格式的字符串值。
GraphQL 服务器应根据他们期望的 ID 格式进行适当的强制转换。当无法进行强制转换时,它们必须引发字段错误。
输入强制转换
当预期作为输入类型时,任何字符串(例如 "4"
)或整数(例如 4
)输入值都应根据给定 GraphQL 服务器期望的 ID 格式强制转换为 ID。任何其他输入值,包括浮点输入值(例如 4.0
),都必须引发查询错误,指示类型不正确。
GraphQL 查询是分层的和组合的,描述了一个信息树。虽然标量类型描述了这些分层查询的叶子值,但对象描述了中间级别。
GraphQL 对象表示命名字段的列表,每个字段都产生特定类型的值。对象值应序列化为有序映射,其中查询的字段名称(或别名)是键,评估字段的结果是值,按它们在查询中出现的顺序排序。
例如,类型 Person
可以描述为
type Person {
name: String
age: Int
picture: Url
}
其中 name
是一个将产生 String
值的字段,age
是一个将产生 Int
值的字段,picture
是一个将产生 Url
值的字段。
对象值的查询必须至少选择一个字段。字段的选择将产生一个有序映射,其中恰好包含查询对象的子集,该子集应按它们被查询的顺序表示。只有在对象类型上声明的字段才能在该对象上有效查询。
例如,选择 Person
的所有字段
{
name
age
picture
}
将产生对象
{
"name": "Mark Zuckerberg",
"age": 30,
"picture": "http://some.cdn/picture.jpg"
}
而选择字段的子集
{
age
name
}
必须仅产生该子集
{
"age": 30,
"name": "Mark Zuckerberg"
}
对象类型的字段可以是标量、枚举、另一个对象类型、接口或联合。此外,它可以是任何包装类型,其底层基本类型是这五种类型之一。
例如,Person
类型可能包含 relationship
type Person {
name: String
age: Int
picture: Url
relationship: Person
}
有效查询必须为返回对象的字段提供嵌套字段集,因此此查询无效
{
name
relationship
}
但是,此示例有效
{
name
relationship {
name
}
}
并将产生每个查询的对象类型的子集
{
"name": "Mark Zuckerberg",
"relationship": {
"name": "Priscilla Chan"
}
}
字段排序
查询对象时,字段的结果映射在概念上与查询执行期间遇到的顺序相同,不包括类型不适用的片段以及通过 @skip
或 @include
指令跳过的字段或片段。当使用 CollectFields() 算法时,会正确生成此排序。
能够表示有序映射的响应序列化格式应保持此排序。只能表示无序映射的序列化格式应在语法上保留此顺序(例如 JSON)。
生成响应,其中字段以它们在请求中出现的相同顺序表示,可以提高调试期间的人类可读性,并使在可以预测属性顺序的情况下更有效地解析响应。
如果片段在其他字段之前展开,则该片段指定的字段在响应中出现在后续字段之前。
{
foo
...Frag
qux
}
fragment Frag on Query {
bar
baz
}
产生有序结果
{
"foo": 1,
"bar": 2,
"baz": 3,
"qux": 4
}
如果在选择中多次查询字段,则按首次遇到的时间排序。但是,类型不适用的片段不影响排序。
{
foo
...Ignored
...Matching
bar
}
fragment Ignored on UnknownType {
qux
baz
}
fragment Matching on Query {
bar
qux
foo
}
产生有序结果
{
"foo": 1,
"bar": 2,
"qux": 3
}
此外,如果指令导致字段被排除,则在字段的排序中不考虑它们。
{
foo @skip(if: true)
bar
foo
}
产生有序结果
{
"bar": 1,
"foo": 2
}
结果强制转换
确定强制转换对象的结果是 GraphQL 执行器的核心,因此这将在规范的该部分中介绍。
输入强制转换
对象永远不是有效的输入。
对象字段在概念上是产生值的函数。有时,对象字段可以接受参数以进一步指定返回值。对象字段参数定义为所有可能的参数名称及其预期输入类型的列表。
例如,具有 picture
字段的 Person
类型可以接受参数来确定要返回的图像大小。
type Person {
name: String
picture(size: Int): Url
}
GraphQL 查询可以选择性地为其字段指定参数以提供这些参数。
此示例查询
{
name
picture(size: 600)
}
可能产生结果
{
"name": "Mark Zuckerberg",
"picture": "http://some.cdn/picture_600.jpg"
}
对象字段参数的类型可以是任何输入类型。
对象中的字段可以根据应用程序的需要标记为已弃用。查询这些字段仍然是合法的(以确保现有客户端不会因更改而中断),但应在文档和工具中适当处理这些字段。
如果对象类型定义不正确,则可能无效。GraphQL 模式中的每个对象类型都必须遵守这组规则。
GraphQL 接口表示命名字段及其参数的列表。然后,GraphQL 对象可以实现接口,这保证它们将包含指定的字段。
GraphQL 接口上的字段与 GraphQL 对象上的字段具有相同的规则;它们的类型可以是标量、对象、枚举、接口或联合,或任何包装类型,其基本类型是这五种类型之一。
例如,接口可以描述必需的字段,然后诸如 Person
或 Business
之类的类型可以实现此接口。
interface NamedEntity {
name: String
}
type Person implements NamedEntity {
name: String
age: Int
}
type Business implements NamedEntity {
name: String
employeeCount: Int
}
当期望多种对象类型之一但应保证某些字段时,产生接口的字段很有用。
继续该示例,Contact
可能引用 NamedEntity
。
type Contact {
entity: NamedEntity
phoneNumber: String
address: String
}
这使我们可以编写一个 Contact
的查询,该查询可以选择公共字段。
{
entity {
name
}
phoneNumber
}
在接口类型上查询字段时,只能查询在该接口上声明的字段。在上面的示例中,entity
返回 NamedEntity
,并且 name
在 NamedEntity
上定义,因此它是有效的。但是,以下查询将无效
{
entity {
name
age
}
phoneNumber
}
因为 entity
引用 NamedEntity
,并且 age
未在该接口上定义。仅当 entity
的结果为 Person
时,查询 age
才有效;查询可以使用片段或内联片段来表达这一点
{
entity {
name
... on Person {
age
}
},
phoneNumber
}
结果强制转换
接口类型应具有某种方式来确定给定结果对应于哪个对象。一旦完成,接口的结果强制转换与对象的结果强制转换相同。
输入强制转换
接口永远不是有效的输入。
如果接口类型定义不正确,则可能无效。
GraphQL 联合表示一个对象,该对象可能是 GraphQL 对象类型列表中的一种,但未在这些类型之间提供任何保证的字段。它们也不同于接口,因为对象类型声明它们实现哪些接口,但不知道哪些联合包含它们。
对于接口和对象,只能直接查询在该类型上定义的字段;要在接口上查询其他字段,必须使用类型化的片段。这与联合相同,但是联合不定义任何字段,因此没有字段可以在不使用类型化片段的情况下在此类型上查询。
例如,我们可能有以下类型系统
union SearchResult = Photo | Person
type Person {
name: String
age: Int
}
type Photo {
height: Int
width: Int
}
type SearchQuery {
firstSearchResult: SearchResult
}
当查询 SearchQuery
类型的 firstSearchResult
字段时,查询将要求片段内的所有字段指示适当的类型。如果查询希望结果是 Person 时的名称,以及结果是照片时的高度,则以下查询无效,因为联合本身未定义任何字段
{
firstSearchResult {
name
height
}
}
相反,查询应该是
{
firstSearchResult {
... on Person {
name
}
... on Photo {
height
}
}
}
结果强制转换
联合类型应具有某种方式来确定给定结果对应于哪个对象。一旦完成,联合的结果强制转换与对象的结果强制转换相同。
输入强制转换
联合永远不是有效的输入。
如果联合类型定义不正确,则可能无效。
GraphQL 枚举是标量类型的一种变体,它表示一组有限的可能值之一。
GraphQL 枚举不是数值的引用,而是其自身独有的值。它们序列化为字符串:即所表示值的名称。
结果强制转换
GraphQL 服务器必须返回一组已定义的可能值之一。如果无法进行合理的强制转换,则必须引发字段错误。
输入强制转换
GraphQL 具有一个常量字面量来表示枚举输入值。GraphQL 字符串字面量不得作为枚举输入接受,而应引发查询错误。
对于非字符串符号值具有不同表示形式的查询变量传输序列化(例如,EDN)应仅允许此类值作为枚举输入值。否则,对于大多数不这样做的传输序列化,字符串可以解释为具有相同名称的枚举输入值。
字段可以定义客户端在查询时传递的参数,以配置其行为。这些输入可以是字符串或枚举,但有时需要比这更复杂。
上面定义的 Object
类型不适合在此处重用,因为 Object
可以包含表达循环引用或对接口和联合的引用的字段,这两种情况都不适合用作输入参数。因此,输入对象在系统中具有单独的类型。
输入对象
定义了一组输入字段;输入字段可以是标量、枚举或其他输入对象。这允许参数接受任意复杂的结构。
结果强制转换
输入对象永远不是有效的结果。
输入强制转换
输入对象的值应为输入对象字面量或无序映射,否则应抛出错误。此无序映射不应包含任何名称未由此输入对象类型的字段定义的条目,否则应抛出错误。
如果输入对象定义的任何非空字段在原始值中没有相应的条目,或者为变量提供了未提供值的变量,或者为变量提供了值 null,则应抛出错误。
强制转换的结果是一个环境特定的无序映射,为输入对象类型定义和原始值提供的每个字段定义了槽位。
对于输入对象类型的每个字段,如果原始值具有同名的条目,并且该条目处的值是字面量值或已提供运行时值的变量,则会将一个条目添加到结果中,其名称与该字段的名称相同。
结果中该条目的值是根据输入字段声明的类型输入强制转换规则对原始条目值进行输入强制转换的结果。
以下是类型为输入对象强制转换的示例
input ExampleInputObject {
a: String
b: Int!
}
原始值 | 变量 | 强制转换后的值 |
---|---|---|
{ a: "abc", b: 123 } | null | { a: "abc", b: 123 } |
{ a: 123, b: "123" } | null | { a: "123", b: 123 } |
{ a: "abc" } | null | 错误:缺少必需字段 b |
{ a: "abc", b: null } | null | 错误:b 必须是非空值。 |
{ a: null, b: 1 } | null | { a: null, b: 1 } |
{ b: $var } | { var: 123 } | { b: 123 } |
{ b: $var } | {} | 错误:缺少必需字段 b。 |
{ b: $var } | { var: null } | 错误:b 必须是非空值。 |
{ a: $var, b: 1 } | { var: null } | { a: null, b: 1 } |
{ a: $var, b: 1 } | {} | { b: 1 } |
GraphQL 列表是一种特殊的集合类型,它声明列表中每个项的类型(称为列表的项类型)。列表值序列化为有序列表,其中列表中的每个项都按照项类型进行序列化。要表示字段使用列表类型,项类型用方括号括起来,如下所示:pets: [Pet]
。
结果强制转换
GraphQL 服务器必须返回有序列表作为列表类型的结果。列表中的每个项都必须是项类型的结果强制转换的结果。如果无法进行合理的强制转换,则必须引发字段错误。特别是,如果返回非列表,则强制转换应失败,因为这表明类型系统和实现之间的期望不匹配。
输入强制转换
当预期作为输入时,只有当列表中的每个项都可以被列表的项类型接受时,才接受列表值。
如果作为输入传递给列表类型的值不是列表,也不是 null 值,则应将其强制转换为大小为 1 的列表,其中传递的值是列表中的唯一项。这是为了允许接受“var args”的输入将其输入类型声明为列表;如果只传递一个参数(常见情况),则客户端可以直接传递该值,而不是构造列表。
默认情况下,GraphQL 中的所有类型都是可为空的;对于上述所有类型,null 值都是有效的响应。要声明不允许空值的类型,可以使用 GraphQL 非空类型。此类型包装了一个底层类型,并且此类型的行为与该包装类型完全相同,但 null 对于包装类型而言不是有效响应。尾随感叹号用于表示使用非空类型的字段,如下所示:name: String!
。
可空 vs. 可选
在查询的上下文中,字段始终是可选的,可以省略字段,查询仍然有效。但是,如果查询返回非空类型的字段,则永远不会返回 null 值。
输入(例如字段参数)默认情况下始终是可选的。但是,非空输入类型是必需的。除了不接受值 null 外,它也不接受省略。为了简单起见,可空类型始终是可选的,而非空类型始终是必需的。
结果强制转换
在上述所有结果强制转换中,null 被认为是有效值。要强制转换非空类型的结果,应执行包装类型的强制转换。如果该结果不是 null,则强制转换非空类型的结果就是该结果。如果该结果是 null,则必须引发字段错误。
输入强制转换
如果未提供非空类型的参数或输入对象字段,或者提供了字面量值 null,或者提供了运行时未提供值或提供了值 null 的变量,则必须引发查询错误。
如果为非空类型提供的值是字面量值(null 除外)或非空变量值,则使用包装类型的输入强制转换对其进行强制转换。
{
fieldWithNonNullArg
}
{
fieldWithNonNullArg(nonNullArg: null)
}
query withNullableVariable($var: String) {
fieldWithNonNullArg(nonNullArg: $var)
}
非空类型验证
GraphQL 模式包括执行引擎支持的指令列表。
GraphQL 实现应提供 @skip
和 @include
指令。
@skip
指令可以为字段、片段展开和内联片段提供,并允许在执行期间根据 if 参数描述进行条件排除。
在此示例中,只有当为 $someTest
提供 false
值时,才会查询 experimentalField
。
query myQuery($someTest: Boolean) {
experimentalField @skip(if: $someTest)
}
@include
指令可以为字段、片段展开和内联片段提供,并允许在执行期间根据 if 参数描述进行条件包含。
在此示例中,只有当为 $someTest
提供 true
值时,才会查询 experimentalField
。
query myQuery($someTest: Boolean) {
experimentalField @include(if: $someTest)
}
@skip
和 @include
均不优先于另一个。如果在同一字段或片段上同时提供了 @skip
和 @include
指令,则必须仅在 @skip
条件为 false 且 @include
条件为 true 时才查询它。反过来说,如果 @skip
条件为 true 或 @include
条件为 false,则不得查询字段或片段。GraphQL 模式包括类型,指示查询和变更操作的起始位置。这提供了类型系统的初始入口点。必须始终提供查询类型,并且它是对象基本类型。变更类型是可选的;如果为 null,则表示系统不支持变更。如果提供了变更类型,则它必须是对象基本类型。
查询类型上的字段指示 GraphQL 查询顶层可用的字段。例如,像下面这样的基本 GraphQL 查询
query getMe {
me
}
当为查询起始类型提供的类型具有名为“me”的字段时,此查询有效。类似地
mutation setName {
setName(name: "Zuck") {
newName
}
}
当为变更起始类型提供的类型不为 null,并且具有名为“setName”且带有名为“name”的字符串参数的字段时,此查询有效。
GraphQL 服务器支持对其模式进行内省。此模式本身使用 GraphQL 查询,从而为工具构建创建了一个强大的平台。
以一个简单应用程序的示例查询为例。在这种情况下,有一个 User 类型,具有三个字段:id、name 和 birthday。
例如,给定一个具有以下类型定义的服务器
type User {
id: String
name: String
birthday: Date
}
查询
{
__type(name: "User") {
name
fields {
name
type {
name
}
}
}
}
将返回
{
"__type": {
"name": "User",
"fields": [
{
"name": "id",
"type": { "name": "String" }
},
{
"name": "name",
"type": { "name": "String" }
},
{
"name": "birthday",
"type": { "name": "Date" }
},
]
}
}
GraphQL 内省系统要求的类型和字段在与用户定义的类型和字段相同的上下文中使用时,以两个下划线作为前缀。这是为了避免与用户定义的 GraphQL 类型发生命名冲突。相反,GraphQL 类型系统作者不得定义任何带有两个前导下划线的类型、字段、参数或任何其他类型系统工件。
内省系统中的所有类型都提供类型为 String
的 description
字段,以允许类型设计者发布文档以及功能。GraphQL 服务器可以使用 Markdown 语法返回 description
字段。因此,建议任何显示描述的工具都使用 Markdown 渲染器。
为了支持向后兼容性的管理,GraphQL 字段和枚举值可以指示它们是否已弃用 (isDeprecated: Boolean
) 以及弃用的原因描述 (deprecationReason: String
)。
使用 GraphQL 内省构建的工具应通过隐藏信息或面向开发者的警告来劝阻已弃用的使用,从而尊重弃用。
当针对任何对象、接口或联合进行查询时,GraphQL 通过元字段 __typename: String!
支持在查询中的任何点进行类型名称内省。它返回当前正在查询的对象类型的名称。
当针对接口或联合类型进行查询以识别已返回的可能类型的实际类型时,这最常用。
此字段是隐式的,并且不会出现在任何已定义类型中的字段列表中。
模式内省系统可以从元字段 __schema
和 __type
访问,这些字段可以从查询操作根类型的类型访问。
__schema: __Schema!
__type(name: String!): __Type
这些字段是隐式的,并且不会出现在查询操作根类型的字段列表中。
GraphQL 模式内省系统的模式
type __Schema {
types: [__Type!]!
queryType: __Type!
mutationType: __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
FIELD
FRAGMENT_DEFINITION
FRAGMENT_SPREAD
INLINE_FRAGMENT
}
__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
可以定义为
type Point {
x: Int
y: Int
}
字段
kind
必须返回 __TypeKind.INPUT_OBJECT
。name
必须返回 String。description
可以返回 String 或 null。inputFields
:InputValue
的列表。列表表示 GraphQL 中的值序列。列表类型是一种类型修饰符:它将另一个类型实例包装在 ofType
字段中,该字段定义列表中每个项的类型。
字段
kind
必须返回 __TypeKind.LIST
。ofType
:任何类型。GraphQL 类型是可空的。值 null 是字段类型的有效响应。
非空类型是一种类型修饰符:它将另一个类型实例包装在 ofType
字段中。非空类型不允许 null 作为响应,并指示参数和输入对象字段的必需输入。
kind
必须返回 __TypeKind.NON_NULL
。ofType
:除非空类型以外的任何类型。列表和非空可以组合,表示更复杂的类型。
如果列表的修饰类型是非空,则该列表不得包含任何 null 项。
如果非空的修饰类型是列表,则不接受 null,但接受空列表。
如果列表的修饰类型是列表,则第一个列表中的每个项都是第二个列表类型的另一个列表。
非空类型不能修改另一个非空类型。
__Field
类型表示对象或接口类型中的每个字段。
字段
name
必须返回 Stringdescription
可以返回 String 或 nullargs
返回 __InputValue
的列表,表示此字段接受的参数。type
必须返回 __Type
,表示此字段返回的值的类型。isDeprecated
返回 true 如果不应再使用此字段,否则返回 false。deprecationReason
可选地提供此字段已弃用的原因。__InputValue
类型表示字段和指令参数以及输入对象的 inputFields
。
字段
name
必须返回 Stringdescription
可以返回 String 或 nulltype
必须返回 __Type
,表示此输入值期望的类型。defaultValue
可以返回字符串编码(使用 GraphQL 语言),表示在运行时未提供值的情况下,此输入值使用的默认值。如果此输入值没有默认值,则返回 null。__Directive
类型表示服务器支持的指令。
字段
name
必须返回 Stringdescription
可以返回 String 或 nulllocations
返回 __DirectiveLocation
的列表,表示可以放置此指令的有效位置。args
返回 __InputValue
的列表,表示此指令接受的参数。GraphQL 不仅验证请求在语法上是否正确,还确保在给定 GraphQL 模式的上下文中,请求是明确且无错误的。
无效的请求在技术上仍然是可执行的,并且将始终产生由执行部分中的过程定义的稳定结果,但是相对于包含验证错误的请求,该结果可能是模糊的、令人惊讶的或意外的,因此不应执行无效的请求。
通常,验证在紧接执行之前的请求上下文中执行,但是,如果已知完全相同的请求之前已验证过,则 GraphQL 服务可以执行请求而无需显式验证。例如:请求可以在开发期间进行验证,前提是它稍后不会更改,或者服务可以验证一次请求并记住结果,以避免将来再次验证相同的请求。任何客户端或开发时工具都应报告验证错误,并且不允许制定或执行已知在该给定时间点无效的请求。
类型系统演变
随着 GraphQL 类型系统模式随着时间的推移通过添加新类型和新字段而演变,以前有效的请求可能后来变得无效。任何可能导致先前有效的请求变为无效的更改都被认为是破坏性更改。鼓励 GraphQL 服务和模式维护者避免破坏性更改,但是为了对这些破坏性更改更具弹性,复杂的 GraphQL 系统可能仍然允许执行在某些时候已知没有验证错误并且自那时以来没有更改的请求。
示例
对于本模式的本节,我们将假定以下类型系统以演示示例
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
type QueryRoot {
dog: Dog
}
形式规范
解释性文本
每个命名的操作定义在其名称引用时,在文档中必须是唯一的。
例如,以下文档是有效的
query getDogName {
dog {
name
}
}
query getOwnerName {
dog {
owner {
name
}
}
}
而以下文档是无效的
query getName {
dog {
name
}
}
query getName {
dog {
owner {
name
}
}
}
即使每个操作的类型不同,它也是无效的
query dogOperation {
dog {
name
}
}
mutation dogOperation {
mutateDog {
id
}
}
形式规范
解释性文本
当文档中仅存在一个查询操作时,GraphQL 允许使用简写形式来定义查询操作。
例如,以下文档是有效的
{
dog {
name
}
}
而以下文档是无效的
{
dog {
name
}
}
query getName {
dog {
owner {
name
}
}
}
形式规范
解释性文本
字段选择的目标字段必须在选择集的作用域类型上定义。别名名称没有限制。
例如,以下片段将无法通过验证
fragment fieldNotDefined on Dog {
meowVolume
}
fragment aliasedLyingFieldTargetNotDefined on Dog {
barkVolume: kawVolume
}
对于接口,直接字段选择只能在字段上完成。具体实现者的字段与给定接口类型选择集的有效性无关。
例如,以下是有效的
fragment interfaceFieldSelection on Pet {
name
}
而以下是无效的
fragment definedOnImplementorsButNotInterface on Pet {
nickname
}
由于联合不定义字段,因此不得直接从联合类型选择集中选择字段,元字段 __typename 除外。来自联合类型选择集的字段必须仅通过片段间接查询。
例如,以下是有效的
fragment inDirectFieldSelectionOnUnion on CatOrDog {
__typename
... on Pet {
name
}
... on Dog {
barkVolume
}
}
但以下是无效的
fragment directFieldSelectionOnUnion on CatOrDog {
name
barkVolume
}
形式规范
解释性文本
如果在执行期间遇到具有相同响应名称的多个字段选择,则要执行的字段和参数以及结果值应是明确的。因此,对于同一对象可能遇到的任何两个字段选择,只有当它们等效时才有效。
对于简单的手写 GraphQL,此规则显然是明显的开发者错误,但是嵌套片段可能会使手动检测变得困难。
以下选择正确合并
fragment mergeIdenticalFields on Dog {
name
name
}
fragment mergeIdenticalAliasesAndFields on Dog {
otherName: name
otherName: name
}
以下无法合并
fragment conflictingBecauseAlias on Dog {
name: nickname
name
}
如果具有相同的参数,则相同的参数也会合并。值和变量都可以正确合并。
例如,以下正确合并
fragment mergeIdenticalFieldsWithIdenticalArgs on Dog {
doesKnowCommand(dogCommand: SIT)
doesKnowCommand(dogCommand: SIT)
}
fragment mergeIdenticalFieldsWithIdenticalValues on Dog {
doesKnowCommand(dogCommand: $dogCommand)
doesKnowCommand(dogCommand: $dogCommand)
}
以下无法正确合并
fragment 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
}
以下字段不会合并在一起,但是两者都不会在同一对象上遇到,因此它们是安全的
fragment 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
fragment conflictingDifferingResponses on Pet {
... on Dog {
someValue: nickname
}
... on Cat {
someValue: meowVolume
}
}
形式规范
解释性文本
标量上的字段选择永远不允许:标量是任何 GraphQL 查询的叶节点。
以下是有效的。
fragment scalarSelection on Dog {
barkVolume
}
以下是无效的。
fragment scalarSelectionsNotAllowedOnBoolean on Dog {
barkVolume {
sinceWhen
}
}
相反,GraphQL 查询的叶字段选择必须是标量。不允许在没有子字段的对象、接口和联合上进行叶选择。
让我们假设以下添加到模式的查询根类型
extend type QueryRoot {
human: Human
pet: Pet
catOrDog: CatOrDog
}
以下示例是无效的
query directQueryOnObjectWithoutSubFields {
human
}
query directQueryOnInterfaceWithoutSubFields {
pet
}
query directQueryOnUnionWithoutSubFields {
catOrDog
}
参数提供给字段和指令。以下验证规则在这两种情况下都适用。
形式规范
解释性文本
提供给字段或指令的每个参数都必须在字段或指令的可能参数集中定义。
例如,以下是有效的
fragment argOnRequiredArg on Dog {
doesKnowCommand(dogCommand: SIT)
}
fragment argOnOptional on Dog {
isHousetrained(atOtherHomes: true) @include(if: true)
}
以下是无效的,因为 command
未在 DogCommand
上定义。
fragment invalidArgName on Dog {
doesKnowCommand(command: CLEAN_UP_HOUSE)
}
这也是无效的,因为 unless
未在 @include
上定义。
fragment invalidArgName on Dog {
isHousetrained(atOtherHomes: true) @include(unless: false)
}
为了探索更复杂的参数示例,让我们将以下内容添加到我们的类型系统中
type 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]
}
extend type QueryRoot {
arguments: Arguments
}
参数中的顺序无关紧要。因此,以下示例都是有效的。
fragment multipleArgs on Arguments {
multipleReqs(x: 1, y: 2)
}
fragment multipleArgsReverseOrder on Arguments {
multipleReqs(y: 1, x: 2)
}
字段和指令将参数视为参数名称到值的映射。参数集中具有相同名称的多个参数是模糊且无效的。
形式规范
形式规范
解释性文本
字面量值必须与它们提供的参数定义的类型兼容,如类型系统章节中定义的强制转换规则所示。
例如,Int 可以强制转换为 Float。
fragment goodBooleanArg on Arguments {
booleanArgField(booleanArg: true)
}
fragment coercedIntIntoFloatArg on Arguments {
floatArgField(floatArg: 1)
}
不可强制转换的转换是从字符串到整数。因此,以下示例是无效的。
fragment stringIntoInt on Arguments {
intArgField(intArg: "3")
}
解释性文本
参数可以是必需的。如果参数的类型为非空,则参数是必需的。如果不是非空,则参数是可选的。当参数类型为非空且为必需时,也不得提供显式值 null。
例如,以下是有效的
fragment goodBooleanArg on Arguments {
booleanArgField(booleanArg: true)
}
fragment goodNonNullArg on Arguments {
nonNullBooleanArgField(nonNullBooleanArg: true)
}
对于具有可空参数的字段,可以省略参数。
因此,以下查询是有效的
fragment goodBooleanArgDefault on Arguments {
booleanArgField
}
但对于非空参数,这是无效的。
fragment missingRequiredArg on Arguments {
nonNullBooleanArgField
}
提供显式值 null 也是无效的。
fragment missingRequiredArg on Arguments {
notNullBooleanArgField(nonNullBooleanArg: null)
}
形式规范
解释性文本
片段定义通过名称在片段扩展中被引用。为了避免歧义,每个片段的名称在文档中必须是唯一的。
内联片段不被视为片段定义,并且不受此验证规则的影响。
例如,以下文档是有效的
{
dog {
...fragmentOne
...fragmentTwo
}
}
fragment fragmentOne on Dog {
name
}
fragment fragmentTwo on Dog {
owner {
name
}
}
而以下文档是无效的
{
dog {
...fragmentOne
}
}
fragment fragmentOne on Dog {
name
}
fragment fragmentOne on Dog {
owner {
name
}
}
形式规范
解释性文本
片段必须在模式中存在的类型上指定。这适用于命名片段和内联片段。如果它们未在模式中定义,则查询无法通过验证。
例如,以下片段是有效的
fragment correctType on Dog {
name
}
fragment inlineFragment on Dog {
... on Dog {
name
}
}
fragment inlineFragment2 on Dog {
... @include(if: true) {
name
}
}
以下片段无法通过验证
fragment notOnExistingType on NotInSchema {
name
}
fragment inlineNotExistingType on Dog {
... on NotInSchema {
name
}
}
形式规范
解释性文本
片段只能在联合、接口和对象上声明。它们在标量上是无效的。它们只能应用于非叶子字段。此规则适用于内联片段和命名片段。
以下片段声明是有效的
fragment fragOnObject on Dog {
name
}
fragment fragOnInterface on Pet {
name
}
fragment fragOnUnion on CatOrDog {
... on Dog {
name
}
}
以下片段声明是无效的
fragment fragOnScalar on Int {
something
}
fragment inlineFragOnScalar on Dog {
... on Boolean {
somethingElse
}
}
形式规范
解释性文本
定义的片段必须在查询文档中使用。
例如,以下是一个无效的查询文档
fragment nameFragment on Dog { # unused
name
}
{
dog {
name
}
}
字段选择也由将片段扩展到彼此确定。目标片段的选择集与引用目标片段的级别的选择集合并。
形式规范
解释性文本
命名片段扩展必须引用文档中定义的片段。如果扩展的目标未定义,则会发生错误
{
dog {
...undefinedFragment
}
}
形式规范
DetectCycles(fragmentDefinition, visited) :
解释性文本
片段扩展图不得形成任何循环,包括扩展自身。否则,操作可能会无限扩展或在底层数据中的循环上无限执行。
这使会导致无限扩展的片段无效
{
dog {
...nameFragment
}
}
fragment nameFragment on Dog {
name
...barkVolumeFragment
}
fragment barkVolumeFragment on Dog {
barkVolume
...nameFragment
}
如果以上片段被内联,则会导致无限大的
{
dog {
name
barkVolume
name
barkVolume
name
barkVolume
name
# forever...
}
}
这也使在循环数据上执行时会导致无限递归的片段无效
{
dog {
...dogFragment
}
}
fragment dogFragment on Dog {
name
owner {
...ownerFragment
}
}
fragment ownerFragment on Dog {
name
pets {
...dogFragment
}
}
形式规范
解释性文本
片段在类型上声明,并且仅当运行时对象类型与类型条件匹配时才适用。它们也在父类型的上下文中扩展。只有当片段的类型条件可以在父类型中应用时,片段扩展才是有效的。
在对象类型的范围内,唯一有效的对象类型片段扩展是应用于与范围内类型相同的类型。
例如
fragment dogFragment on Dog {
... on Dog {
barkVolume
}
}
而以下是无效的
fragment catInDogFragmentInvalid on Dog {
... on Cat {
meowVolume
}
}
在对象类型的范围内,如果对象类型实现了接口或是联合的成员,则可以使用联合或接口扩展。
例如
fragment petNameFragment on Pet {
name
}
fragment interfaceWithinObjectFragment on Dog {
...petNameFragment
}
是有效的,因为 Dog 实现了 Pet 接口。
同样
fragment catOrDogNameFragment on CatOrDog {
... on Cat {
meowVolume
}
}
fragment unionWithObjectFragment on Dog {
...catOrDogNameFragment
}
是有效的,因为 Dog 是 CatOrDog 联合的成员。值得注意的是,如果检查 CatOrDogNameFragment 的内容,您可能会注意到永远不会返回有效的结果。但是,我们不将其指定为无效,因为我们只考虑片段声明,而不考虑其主体。
联合或接口扩展可以在对象类型片段的上下文中使用,但前提是对象类型是该接口或联合的可能类型之一。
例如,以下片段是有效的
fragment petFragment on Pet {
name
... on Dog {
barkVolume
}
}
fragment catOrDogFragment on CatOrDog {
... on Cat {
meowVolume
}
}
petFragment 是有效的,因为 Dog 实现了 Pet 接口。catOrDogFragment 是有效的,因为 Cat 是 CatOrDog 联合的成员。
相比之下,以下片段是无效的
fragment sentientFragment on Sentient {
... on Dog {
barkVolume
}
}
fragment humanOrAlienFragment on HumanOrAlien {
... on Cat {
meowVolume
}
}
Dog 未实现 Sentient 接口,因此 sentientFragment 永远不会返回有意义的结果。因此,该片段是无效的。同样,Cat 不是 HumanOrAlien 联合的成员,它也永远不会返回有意义的结果,使其无效。
联合或接口片段可以在彼此之间使用。只要在作用域和扩展的可能类型的交集中存在至少一个对象类型,该扩展就被认为是有效的。
因此,例如
fragment unionWithInterface on Pet {
...dogOrHumanFragment
}
fragment dogOrHumanFragment on DogOrHuman {
... on Dog {
barkVolume
}
}
被认为是有效的,因为 Dog 实现了 Pet 接口,并且是 DogOrHuman 的成员。
然而
fragment nonIntersectingInterfaces on Pet {
...sentientFragment
}
fragment sentientFragment on Sentient {
name
}
是无效的,因为不存在实现 Pet 和 Sentient 两者的类型。
形式规范
解释性文本
输入对象不得包含多个同名字段,否则会存在歧义,其中包括语法的忽略部分。
例如,以下查询将无法通过验证。
{
field(arg: { field: true, field: false })
}
形式规范
解释性文本
GraphQL 服务器定义了它们支持的指令。对于指令的每次使用,该指令必须在该服务器上可用。
形式规范
解释性文本
GraphQL 服务器定义了它们支持的指令以及它们在何处支持这些指令。对于指令的每次使用,该指令必须在服务器已声明支持的位置使用。
例如,以下查询将无法通过验证,因为 @skip
未提供 QUERY
作为有效位置。
query @skip(if: $foo) {
field
}
形式规范
解释性文本
指令用于描述它们所应用的定义上的某些元数据或行为更改。当使用多个同名指令时,预期的元数据或行为变得模糊,因此每个位置只允许一个指令。
例如,以下查询将无法通过验证,因为 @skip
已对同一字段使用了两次
query ($foo: Boolean = true, $bar: Boolean = false) {
field @skip(if: $foo) @skip(if: $bar)
}
但是,以下示例是有效的,因为 @skip
每个位置仅使用一次,尽管在查询中和在同一命名字段上使用了两次
query ($foo: Boolean = true, $bar: Boolean = false) {
field @skip(if: $foo) {
subfieldA
}
field @skip(if: $bar) {
subfieldB
}
}
形式规范
解释性文本
如果任何操作定义了多个同名变量,则它是模糊且无效的。即使重复变量的类型相同,它也是无效的。
query houseTrainedQuery($atOtherHomes: Boolean, $atOtherHomes: Boolean) {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
多个操作定义具有相同名称的变量是有效的。如果两个操作引用同一个片段,则实际上可能是必要的
query A($atOtherHomes: Boolean) {
...HouseTrainedFragment
}
query B($atOtherHomes: Boolean) {
...HouseTrainedFragment
}
fragment HouseTrainedFragment {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
形式规范
解释性文本
操作定义的变量如果该变量的类型不是非空类型,则允许定义默认值。
例如,以下查询将通过验证。
query houseTrainedQuery($atOtherHomes: Boolean = true) {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
但是,如果变量定义为非空类型,则默认值是不可达的。因此,诸如以下的查询将无法通过验证
query houseTrainedQuery($atOtherHomes: Boolean! = true) {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
默认值必须与变量的类型兼容。类型必须匹配,或者它们必须能够强制转换为该类型。
不匹配的类型将失败,例如在以下示例中
query houseTrainedQuery($atOtherHomes: Boolean = "true") {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
但是,如果类型是可强制转换的,则查询将通过验证。
例如
query intToFloatQuery($floatVar: Float = 1) {
arguments {
floatArgField(floatArg: $floatVar)
}
}
形式规范
解释性文本
变量只能是标量、枚举、输入对象或这些类型的列表和非空变体。这些被称为输入类型。对象、联合和接口不能用作输入。
对于这些示例,请考虑以下类型系统添加
input ComplexInput { name: String, owner: String }
extend type QueryRoot {
findDog(complex: ComplexInput): Dog
booleanList(booleanListArg: [Boolean!]): Boolean
}
以下查询是有效的
query takesBoolean($atOtherHomes: Boolean) {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
query takesComplexInput($complexInput: ComplexInput) {
findDog(complex: $complexInput) {
name
}
}
query TakesListOfBooleanBang($booleans: [Boolean!]) {
booleanList(booleanListArg: $booleans)
}
以下查询是无效的
query takesCat($cat: Cat) {
# ...
}
query takesDogBang($dog: Dog!) {
# ...
}
query takesListOfPet($pets: [Pet]) {
# ...
}
query takesCatOrDog($catOrDog: CatOrDog) {
# ...
}
形式规范
解释性文本
变量的作用域是基于每个操作的。这意味着在操作上下文中使用的任何变量都必须在该操作的顶层定义
例如
query variableIsDefined($atOtherHomes: Boolean) {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
是有效的。$atOtherHomes 由操作定义。
相比之下,以下查询是无效的
query variableIsNotDefined {
dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
}
$atOtherHomes 未由操作定义。
片段使此规则变得复杂。操作传递包含的任何片段都可以访问该操作定义的变量。片段可以出现在多个操作中,因此变量用法必须与所有这些操作中的变量定义相对应。
例如,以下是有效的
query variableIsDefinedUsedInSingleFragment($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
因为 isHousetrainedFragment 在操作 variableIsDefinedUsedInSingleFragment 的上下文中被使用,并且变量由该操作定义。
另一方面,如果片段包含在不定义引用变量的操作中,则查询无效。
query variableIsNotDefinedUsedInSingleFragment {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
这也传递适用,因此以下查询也失败
query variableIsNotDefinedUsedInNestedFragment {
dog {
...outerHousetrainedFragment
}
}
fragment outerHousetrainedFragment on Dog {
...isHousetrainedFragment
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
变量必须在片段使用的所有操作中定义。
query housetrainedQueryOne($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
query housetrainedQueryTwo($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
但是,以下查询无法通过验证
query housetrainedQueryOne($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
query housetrainedQueryTwoNotDefined {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
这是因为 housetrainedQueryTwoNotDefined 未定义变量 $atOtherHomes,但该变量被 isHousetrainedFragment 使用,而 isHousetrainedFragment 包含在该操作中。
形式规范
解释性文本
操作定义的所有变量都必须在该操作或该操作传递包含的片段中使用。未使用的变量会导致验证错误。
例如,以下查询是无效的
query variableUnused($atOtherHomes: Boolean) {
dog {
isHousetrained
}
}
因为 $atOtherHomes 未被引用。
这些规则也适用于传递片段扩展
query variableUsedInFragment($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
以上是有效的,因为 $atOtherHomes 在 isHousetrainedFragment 中使用,而 isHousetrainedFragment 由 variableUsedInFragment 包含。
如果该片段没有对 $atOtherHomes 的引用,则它将无效
query variableNotUsedWithinFragment($atOtherHomes: Boolean) {
...isHousetrainedWithoutVariableFragment
}
fragment isHousetrainedWithoutVariableFragment on Dog {
isHousetrained
}
文档中的所有操作都必须使用其所有变量。
因此,以下文档无法通过验证。
query queryWithUsedVar($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
query queryWithExtraVar($atOtherHomes: Boolean, $extra: Int) {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
此文档无效,因为 queryWithExtraVar 定义了一个多余的变量。
形式规范
解释性文本
变量用法必须与它们传递到的参数兼容。
当变量在类型完全不匹配的上下文中使用时,或者当可空类型的变量传递给非空参数类型时,会发生验证失败。
类型必须匹配
query intCannotGoIntoBoolean($intArg: Int) {
arguments {
booleanArgField(booleanArg: $intArg)
}
}
类型为 Int 的 $intArg 不能用作类型为 Boolean 的 booleanArg 的参数。
列表基数也必须相同。例如,列表不能传递到单数值中。
query booleanListCannotGoIntoBoolean($booleanListArg: [Boolean]) {
arguments {
booleanArgField(booleanArg: $booleanListArg)
}
}
空值性也必须受到尊重。通常,可空变量不能传递给非空参数。
query booleanArgQuery($booleanArg: Boolean) {
arguments {
nonNullBooleanArgField(nonNullBooleanArg: $booleanArg)
}
}
一个值得注意的例外是当提供默认参数时。实际上,它们被视为非空类型。
query booleanArgQueryWithDefault($booleanArg: Boolean = true) {
arguments {
nonNullBooleanArgField(nonNullBooleanArg: $booleanArg)
}
}
对于列表类型,关于空值性的相同规则适用于外部类型和内部类型。可空列表不能传递给非空列表,并且可空值列表不能传递给非空值列表。以下是有效的
query nonNullListToList($nonNullBooleanList: [Boolean]!) {
arguments {
booleanListArgField(booleanListArg: $nonNullBooleanList)
}
}
但是,可空列表不能传递给非空列表
query listToNonNullList($booleanList: [Boolean]) {
arguments {
nonNullBooleanListField(nonNullBooleanListArg: $booleanList)
}
}
这将验证失败,因为 [T]
不能传递给 [T]!
。
同样,[T]
不能传递给 [T!]
。
GraphQL 通过执行从请求生成响应。
执行请求包含一些信息
给定此信息,ExecuteRequest() 的结果生成响应,该响应将根据下面的“响应”部分进行格式化。
要执行请求,执行器必须具有已解析的 Document
(如本规范的“查询语言”部分中所定义)和一个选定的操作名称(如果文档定义了多个操作要运行),否则文档应仅包含单个操作。请求的结果由根据下面的“执行操作”部分执行此操作的结果确定。
如“验证”部分所述,只有通过所有验证规则的请求才应执行。如果已知存在验证错误,则应在响应的“errors”列表中报告这些错误,并且请求必须失败,而无需执行。
通常,验证是在紧接执行之前的请求上下文中执行的,但是,如果已知完全相同的请求以前已通过验证,则 GraphQL 服务可以在不立即验证它的情况下执行请求。GraphQL 服务应仅执行在某个时刻已知没有任何验证错误,并且此后未更改的请求。
例如:请求可以在开发期间进行验证,前提是它以后不会更改,或者服务可以验证一次请求并记忆结果,以避免将来再次验证相同的请求。
如果操作定义了任何变量,则需要使用变量声明类型的输入强制规则来强制这些变量的值。如果在变量值的输入强制期间遇到查询错误,则操作将失败,而无需执行。
类型系统(如规范的“类型系统”部分所述)必须提供查询根对象类型。如果支持变更,则它还必须提供变更根对象类型。
如果操作是查询,则操作的结果是使用查询根对象类型执行查询的顶层选择集的结果。
在执行查询时,可以提供初始值。
如果操作是变更,则操作的结果是在变更根对象类型上执行变更的顶层选择集的结果。此选择集应按顺序执行。
预期变更操作中的顶层字段会对底层数据系统执行副作用。顺序执行提供的变更可确保在这些副作用期间防止竞争条件。
要执行选择集,需要知道正在评估的对象值和对象类型,以及是否必须串行执行,或者可以并行执行。
首先,将选择集转换为分组字段集;然后,分组字段集中的每个表示字段都会生成响应映射中的一个条目。
通常,执行器可以以其选择的任何顺序(通常是并行)执行分组字段集中的条目。由于除顶层变更字段以外的字段的解析必须始终是无副作用和幂等的,因此执行顺序不得影响结果,因此服务器可以自由地以其认为最佳的任何顺序执行字段条目。
例如,给定以下要正常执行的分组字段集
{
birthday {
month
}
address {
street
}
}
有效的 GraphQL 执行器可以以其选择的任何顺序解析这四个字段(当然,birthday
必须在 month
之前解析,address
必须在 street
之前解析)。
执行变更时,最顶层选择集中的选择将按串行顺序执行。
当串行执行分组字段集时,执行器必须按分组字段集中提供的顺序考虑分组字段集中的每个条目。它必须在继续分组字段集中的下一个条目之前,确定每个项目的完成对应的结果映射中的条目
例如,给定以下要串行执行的选择集
{
changeBirthday(birthday: $newBirthday) {
month
}
changeAddress(address: $newAddress) {
street
}
}
执行器必须串行地
changeBirthday
的 ExecuteField(),这在 CompleteValue() 期间将正常执行 { month }
子选择集。changeAddress
的 ExecuteField(),这在 CompleteValue() 期间将正常执行 { street }
子选择集。作为一个说明性示例,让我们假设我们有一个变更字段 changeTheNumber
,它返回一个包含一个字段 theNumber
的对象。如果我们串行执行以下选择集
{
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 }
子选择集正确的执行器必须为该选择集生成以下结果
{
"first": {
"theNumber": 1
},
"second": {
"theNumber": 3
},
"third": {
"theNumber": 2
}
}
在执行之前,选择集通过调用 CollectFields() 转换为分组字段集。分组字段集中的每个条目都是共享响应键的字段列表。这确保了通过引用的片段包含的具有相同响应键(别名或字段名称)的所有字段都同时执行。
例如,收集此选择集的字段将收集字段 a
的两个实例和一个字段 b
的实例
{
a {
subfield1
}
...ExampleFragment
}
fragment ExampleFragment on Query {
a {
subfield2
}
b
}
通过 CollectFields() 生成的字段组的深度优先搜索顺序在执行过程中保持不变,从而确保字段以稳定且可预测的顺序出现在执行的响应中。
@skip
,则设 skipDirective 为该指令。@include
,则设 includeDirective 为该指令。在分组字段集中请求的,且在选定 objectType 上定义的每个字段,都将在响应映射中产生一个条目。字段执行首先强制转换任何提供的参数值,然后解析字段的值,最后通过递归执行另一个选择集或强制转换标量值来完成该值。
字段可以包含提供给底层运行时的参数,以便正确生成值。这些参数由类型系统中的字段定义为具有特定的输入类型:标量、枚举、输入对象,或这三者的列表或非空包装的变体。
在查询中的每个参数位置,可以是一个字面量值或一个在运行时提供的变量。
虽然几乎所有的 GraphQL 执行都可以被通用地描述,但最终,暴露 GraphQL 接口的内部系统必须提供值。这通过 ResolveFieldValue 公开,它为一个类型上的给定字段生成一个实际值。
例如,这可能接受 objectType Person
,field "soulMate",以及代表 John Lennon 的 objectValue。预计它将产生代表 Yoko Ono 的值。
在解析字段的值之后,通过确保它符合预期的返回类型来完成它。如果返回类型是另一个对象类型,则字段执行过程将递归地继续。
解析抽象类型
当完成具有抽象返回类型的字段时,即接口或联合返回类型,首先必须将抽象类型解析为相关的对象类型。这种确定由内部系统使用任何适当的方式进行。
合并选择集
当并行执行多个同名字段时,它们的选择集在完成值时合并在一起,以便继续执行子选择集。
一个示例查询,说明具有相同名称和子选择的并行字段。
{
me {
firstName
}
me {
lastName
}
}
在解析 me
的值之后,选择集合并在一起,以便可以为一个值解析 firstName
和 lastName
。
如果在解析字段时抛出错误,则应将其视为字段返回了 null,并且必须将错误添加到响应中的 "errors" 列表中。
如果解析字段的结果是 null (无论是由于解析字段的函数返回 null 还是由于发生了错误),并且该字段是 Non-Null
类型,则会抛出一个字段错误。该错误必须添加到响应中的 "errors" 列表中。
如果字段由于已添加到响应中的 "errors" 列表中的错误而返回 null,则 "errors" 列表不得进一步受到影响。也就是说,每个字段只能向 errors 列表添加一个错误。
由于 Non-Null
类型字段不能为 null,因此字段错误会传播到父字段进行处理。如果父字段可以为 null,则它解析为 null;否则,如果它是 Non-Null
类型,则字段错误会进一步传播到其父字段。
如果从请求的根到错误源的所有字段都返回 Non-Null
类型,则响应中的 "data" 条目应为 null。
当 GraphQL 服务器收到请求时,它必须返回一个格式良好的响应。服务器的响应描述了如果成功执行请求的操作的结果,并描述了请求期间遇到的任何错误。
在字段上发生错误并被替换为 null 的情况下,响应可能既包含部分响应,也包含遇到的错误。
GraphQL 不要求特定的序列化格式。但是,客户端应使用支持 GraphQL 响应中主要原语的序列化格式。特别是,序列化格式必须支持以下四个原语的表示
可以表示有序映射的序列化格式应保留执行部分中 CollectFields() 定义的请求字段的顺序。只能表示无序映射的序列化格式应在语法上保留此顺序(例如 JSON)。
生成响应,其中字段以它们在请求中出现的相同顺序表示,可以提高调试期间的人类可读性,并使在可以预测属性顺序的情况下更有效地解析响应。
序列化格式可以支持以下原语,但是,字符串可以用作这些原语的替代。
JSON 是 GraphQL 首选的序列化格式,但如上所述,GraphQL 不要求特定的序列化格式。为了保持一致性和易于表示,整个规范中都以 JSON 给出响应示例。特别是,在我们的 JSON 示例中,我们将使用以下 JSON 概念来表示原语
GraphQL 值 | JSON 值 |
---|---|
映射 | 对象 |
列表 | 数组 |
Null | null |
字符串 | 字符串 |
布尔值 | true 或 false |
整数 | 数字 |
浮点数 | 数字 |
枚举值 | 字符串 |
对象属性排序
虽然 JSON 对象被指定为 键值对的无序集合,但这些对以有序的方式表示。换句话说,虽然 JSON 字符串 { "name": "Mark", "age": 30 }
和 { "age": 30, "name": "Mark" }
编码相同的值,但它们也具有可观察到的不同属性排序。
由于评估选择集的结果是有序的,因此序列化的 JSON 对象应通过以与查询执行定义的请求字段相同的顺序写入对象属性来保留此顺序。
例如,如果查询是 { name, age }
,则以 JSON 响应的 GraphQL 服务器应响应 { "name": "Mark", "age": 30 }
,而不应响应 { "age": 30, "name": "Mark" }
。
对 GraphQL 操作的响应必须是一个映射。
如果操作包括执行,则响应映射必须包含第一个条目,键为 data
。此条目的值在“数据”部分中描述。如果操作在执行之前失败,由于语法错误、缺少信息或验证错误,则此条目不得出现。
如果操作遇到任何错误,则响应映射必须包含下一个条目,键为 errors
。此条目的值在“错误”部分中描述。如果操作在没有遇到任何错误的情况下完成,则此条目不得出现。
响应映射还可以包含一个键为 extensions
的条目。此条目(如果设置)必须具有一个映射作为其值。此条目保留给实现者以他们认为合适的方式扩展协议,因此对其内容没有额外的限制。
为确保未来对协议的更改不会破坏现有的服务器和客户端,顶层响应映射不得包含除上述三个条目之外的任何条目。
响应中的 data
条目将是执行请求操作的结果。如果操作是查询,则此输出将是模式的查询根类型的对象;如果操作是 mutation,则此输出将是模式的 mutation 根类型的对象。
如果在执行开始之前遇到错误,则 data
条目不应出现在结果中。
如果在执行期间遇到阻止有效响应的错误,则响应中的 data
条目应为 null
。
响应中的 errors
条目是一个非空的错误列表,其中每个错误都是一个映射。
如果在请求的操作期间没有遇到任何错误,则 errors
条目不应出现在结果中。
每个错误都必须包含一个键为 message
的条目,其中包含错误的字符串描述,旨在为开发人员提供理解和纠正错误的指南。
如果错误可以与请求的 GraphQL 文档中的特定点关联,则它应包含一个键为 locations
的条目,其中包含位置列表,其中每个位置都是一个映射,键为 line
和 column
,两者都是从 1
开始的正数,用于描述关联的语法元素的开始。
GraphQL 服务器可以根据需要为错误提供额外的条目,以产生更有帮助或机器可读的错误,但未来版本的规范可能会描述错误的附加条目。
如果响应中的 data
条目为 null
或不存在,则响应中的 errors
条目不能为空。它必须包含至少一个错误。它包含的错误应指示为什么无法返回数据。
如果响应中的 data
条目不是 null
,则响应中的 errors
条目可能包含执行期间发生的任何错误。如果执行期间发生错误,则应包含这些错误。
本规范文档包含许多符号约定,用于描述技术概念,例如语言语法和语义以及运行时算法。
本附录旨在更详细地解释这些符号,以避免歧义。
上下文无关文法由许多产生式组成。每个产生式都有一个抽象符号,称为“非终结符”作为其左侧,以及零个或多个非终结符和/或终结字符的可能序列作为其右侧。
从单个目标非终结符开始,上下文无关文法描述了一种语言:可以通过重复地将目标序列中的任何非终结符替换为它定义的序列之一,直到所有非终结符都被终结字符替换为止,来描述的字符的可能序列的集合。
终结符在本文档中以等宽字体用两种形式表示:特定的 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]”为前缀时,一些可能的序列可以有条件地包含或排除。
作为一个例子
是简写形式
本规范以算法步骤列表的形式描述了许多文法产生式的语义值。
例如,这描述了解析器应如何解释字符串字面量
本规范描述了静态和运行时语义使用的一些算法,它们以类似函数的语法形式以及要执行的算法步骤列表来定义。
例如,这描述了给定运行时 objectType 和片段的 fragmentType,是否应将片段扩展到位
! | $ | ( | ) | ... | : | = | @ | [ | ] | { | | | } |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+ | - |
" | \ | / | b | f | n | r | t |
query | mutation |
true | false |