GraphQL语言致力于提供一种直观的弹性语法系统,用以描述客户端程序设计时的数据需求以及数据交互行为。
例如,下面这个GraphQL请求将会从Facebook的GraphQL实现中获取id为4的用户的名字。
{
user(id: 4) {
name
}
}
将会产生如下JSON数据:
{
"user": {
"name": "Mark Zuckerberg"
}
}
GraphQL并不能像编程语言一样执行任意计算,但能针对具有本规范所述能力的应用服务器进行查询。GraphQL的实现并不要求应用服务器使用特定的编程语言或者存储系统,而是只需要应用服务器将他们的能力映射为符合GraphQL编码原理的统一语言和类型系统。这样既为产品开发提供了友好的统一接口,又为工具建设提供了强大平台。
GraphQL有若干设计原则:
基于这些原则,GraphQL在建造客户端应用的时候就成了强大的生产环境。产品开发者和设计师在高质量工具的支撑下,无需阅读大量文档,只需一点或者无需正式训练就能根据GraphQL服务器建造客户端。当然为了完成这个目的,这些服务器和工具的建造者也必不可少。
下文的正式规范即作为这些建造者的参考指南,其描述了语言以及语法,接受查询的类型系统以及内省系统,执行引擎以及验证引擎的算法。本规范的目标是为GraphQL工具、客户端库、服务端实现提供了生态所需的基础和框架,无论组织还是平台,我们都希望和社区通力合作以完成上述目标。
客户端使用GraphQL查询语言来请求GraphQL服务,我们称这些请求为文档,文档包含操作(queries/查询,mutations/更改,和subscriptions订阅)和片段(用于组合重用的共有单元)。
GraphQL文档的语法中,将终端符号视为记号,即独立词法单元。这些记号以词法方式定义,满足源字符模式(用::
定义)。译者案:翻译中使用→表示
源字符 → /[\u0009\u000A\u000D\u0020-\uFFFF]/
GraphQL文档可表示为一序列的Unicode(统一码)字符,然而,除了少许例外,大部分GraphQL文档都是用ASCII非控制字符来表示,以便于尽量兼容已有工具、语言和序列化格式,并尽可能避免在编辑器和源代码管理的显示问题。
UnicodeBOM → “字节顺序标记(U+FEFF)”
GraphQL的StringValue(字符串值)和Comment(备注)中可以使用非ASCII的Unicode字符。
BOM,又称字节顺序标记,是一个特殊的Unicode字符,它出现在文件的头部,以便程序用以确认当前文本流是Unicode编码,使用了大端还是小端,该用哪一种Unicode编码来转义。
空白符 →
空白符出现在记号的前后,作为记号分隔使用,用于提升源文本的易读性。GraphQL查询文档的空白符可能出现在String或Comment记号中,但并不会显著影响其语义。
行终止符 →
跟空白符类似,行终止符也是用于提升源文本的易读性,可出现在记号的前后,对GraphQL查询文档的语义无显著影响。行终止符不应该出现在其他记号之间。
注释 → #
注释字符
注释字符 → 源字符,非行终止符
GraphQL查询文档可以包含以#开头的单行注释。
注释可使用除了LineTerminator(行终止符)以外的任意Unicode字符(码点)。所以可以说,注释包含了以#开头的除行终止符以外的所有字符。
注释与空白符类似,出现在任意记号后面、行终止符前面,对GraphQL查询文档的语义无显著影响。
逗号 → ,
与空白符和行终止符类似,逗号(,)也是提升源文本的易读性、分隔词法记号,对GraphQL查询文档的语法语义上也无显著影响.
无语义逗号保证了无论有误逗号,都不影响文档的解读,在其他语言中这就可能是一个用户错误。为了源代码易读性和可维护性,列表会使用行终止符和末尾逗号作为分隔符,无语义逗号也有这个样式上的用途。
记号 →
一个GraphQL文档由多种独立记号组成,本规范中使用源文本Unicode字符模式来定义这些记号。
在后文GraphQL查询文档句法中,记号将作为终结符使用。
无语义记号 →
在词法记号的前后可能会出现不定量的无语义记号,包括WhiteSpace(空白符)和Comment备注。源文档的无语义区域都是无语义影响的,但是无语义字符可能以一种有影响的方式出现在源字符词法记号之间,譬如String(字符串)可能包含空白字符。
在解析给定记号时,所有字符都不能被忽略,譬如FloatValue(浮点值)的字符中不允许出现空白符。 (译者案:本段无力,求大神指点)
! | $ | ( | ) | ... | : | = | @ | [ | ] | { | | | } |
标点 → ! $ ( ) ... : = @ [ ] { | } 之一
GraphQL文档使用了标点符号以描述结构,GraphQL是一种数据描述语言,而非编程语言,因此GraphQL缺乏用于描述数学表达式的标点符号。
命名 → /[_A‐Za‐z][_0‐9A‐Za‐z]/*
GraphQL查询文档全是命名的产物:operations(操作),fields(字段),arguments(参数),directives(指令), fragments(片段)和variables(变量)。所有命名必须遵循以下格式:
GraphQL的命名是大小写敏感的,也就是说name
,Name
,和NAME
是不同的名字,下划线也具有影响,other_name
和othername
也是两个不同的名字。
GraphQL的命名限制在上述ASCII子集内,以便支持尽可能多的其他系统。
文档 → 定义
定义 →
GraphQL查询文档描述了GraphQL服务收到的完整文件或者请求字符串。一个文档可以包含多个操作和片段的定义。一个查询文档只有包含操作时,服务器才能执行。但是无操作的文档也能被解析和验证,以让客户端提供单个跨文档请求。
如果一个文档只有一个操作,那这个操作可以不带命名或者以简写,省略掉query关键字和操作名。否则当一个查询文档包含多个操作时,每个操作都必须命名,并且在提交给服务器的时候,也要指明需要执行的目标操作。
query | mutation | subscription |
操作定义 →
操作类型 →
query
mutation
subscription
之一 GraphQL做了三类操作模型:每一个操作都以一个可选操作名和选择集合表示,例如这个mutation(更改)操作,对一个story点赞(like),然后获取了被点赞次数:
mutation {
likeStory(storyID: 12345) {
story {
likeCount
}
}
}
Query shorthand/查询简写
如果一个文档只包含一个查询操作,也不包含变量和指令,那么这个操作可以省略query关键字和操作名。例如,下面这个无名查询操作就写成了查询简写形式:
{
field
}
选择集合 → {选择}
选择 →
一个操作选择了他所需要的信息的集合,然后就会精确地得到他所要的信息,没有一点多余,避免了数据的多取或少取。
{
id
firstName
lastName
}
这个query/查询中,id
,firstName
和lastName
字段构成了选择集合,选择集合也能包含fragment/片段的引用。
字段 → 别名?具名参数?指令?选择集合?
一个选择集合主要由字段组成,一个字段描述了选择集合中对请求可用的一个离散信息片段。
有些字段描述了复杂的数据或者与其他数据的关联,为了进一步解明这种数据,一个字段可能包含一个选择集合,从而使能将请求嵌套起来。所有的GraphQL操作都必须依次深入嵌套,指明所有标量值字段,以保证响应数据的形态上没有歧义。
例如,这个操作选择了复杂数据和关联数据,并深入到嵌套内部,直到标量值字段。
{
me {
id
firstName
lastName
birthday {
month
day
}
friends {
name
}
}
}
一个操作中,顶层选择集合的字段通常表示对应用和观察者而言全局可见的信息。典型的案例有顶层字段指向当前登录的观察者,或者引用唯一id来取特定类型数据:
# `me` could represent the currently logged in viewer.`me`指代当前登录的观察者
{
me {
name
}
}
# `user` represents one of many users in a graph of data, referred to by a
# unique identifier.
# `user`表示一个通过id来从图数据中取出来的用户
{
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)
}
}
Arguments are unordered/参数无需顺序
参数可以以任意句法顺序排列,都表示同一种语义。
下列两个查询语义上都是一样的:
{
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"
}
}
顶级query/查询也是一个字段,所以它也可以使用别名:
{
zuck: user(id: 4) {
id
name
}
}
得到这个结果:
{
"zuck": {
"id": 4,
"name": "Mark Zuckerberg"
}
}
如果使用了别名,那么返回对象的中字段的键名就是别名,否则就是字段名。
片段解构/展开 → ... 片段名 指令?
片段定义 → fragment 片段名 类型条件 指令? 选择集
片段名 → 除了on
以外的命名
片段是GraphQL组合拼装的基本单元,它通用选择集字段的重用得以实现,减少了文档中的重复文本。内联片段可以直接在选择集合内使用,通常用于interface(接口)或者union(联合)这种存在类型条件的场合。
例如,我们想要获取某个用户的朋友以及和他互为朋友的人的共通信息:
query noFragments {
user(id: 4) {
friends(first: 10) {
id
name
profilePic(size: 50)
}
mutualFriends(first: 10) {
id
name
profilePic(size: 50)
}
}
}
这些重复的字段可以提取进一个fragment(片段)中,然后被父级fragment(片段)或者query(查询)组合:
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
三个查询都会产生相同的返回对象。
类型条件 → on 具名类型
片段需要指定应用于的目标类型,在上述案例中,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
}
}
}
}
内联片段也用于将指令应用于一群字段的场景。如果省略了类型条件,片段则被视为等同于封装所在的上下文。
query 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
)
+ | - |
指定浮点数需要包含小数点(例如:1.0
)或者指数符号(例如:1e50
)或者两者(例如:6.0221413e23
)。
true | false |
true
和false
两个关键字表示布尔型的两个值。
" | \ | / | b | f | n | r | t |
字符串是一系列由双引号("
)包起来的字符,譬如"Hello World"
。字符串内的空白符和其他无语义字符都对字符串有影响。
Semantics/语义
转义后字符 | 字符单元值 | 字符名称 |
---|---|---|
" | 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
}
前者显式使用了null给arg参数,后者隐式,没有给arg参数以值。两个形式会被不同解读,譬如一个mutation操作中,会表示成删除一个字段或者不改变一个字段。两者都不能用于非空输入类型参数。
枚举值表现为没有引号包裹的名称,规范建议使用全大写字母表示枚举值,枚举值仅用于准确枚举类型可用的上下文中,因此枚举类型命名上不必使用字面量。
列表是包在方括号[ ]
中的有序值序列,列表值可以是任意字面量值或者变量,譬如[1, 2, 3]
。
因为GraphQL中逗号是可选的,因此末尾逗号和重复逗号都是允许的,而不会代表空缺值。
Semantics/语义
输入型对象是无需键值列表,使用花括号{ }
包起来。对象的值可以是输入字面量值或者变量值(例如:{ name: "Hello world", score: 1.0 }
)。我们将输入型对象的字面量表示法称作“对象字面量”。
Input object fields are unordered/输入型对象的字段是无序的
输入型字段能以各种句法顺位排列,而表示相同的语义。
下面两个查询在语义上是一样的:
{
nearestThing(location: { lon: 12.43, lat: -53.211 })
}
{
nearestThing(location: { lat: -53.211, lon: 12.43 })
}
Semantics/语义
变量 → $ 名称
变量定义 → 变量:类型 默认值?
默认值 → 值常量
GraphQL查询可以使用变量作为参数,已最大化查询重用,避免客户端运行时耗费巨大的字符串重建。
如果没有被定义为常量(例如DefaultValue),Variable就能被赋予一个输入类型。
变量必须在查询的顶部定义,并在整个操作的执行周期范围内可用。
下面例子中,我们想要根据特定设备大小获取一个对应大小的档案图片:
query getZuckProfile($devicePicSize: Int) {
user(id: 4) {
id
name
profilePic(size: $devicePicSize)
}
}
在向GraphQL请求的时候,这些参数的值也需要一并发送,以便执行期间用以替换。假设以JSON发送参数,我们请求大小为60
宽度的档案照片:
{
"devicePicSize": 60
}
Variable use within Fragments/片段内的参数
片段内也可以使用查询变量,变量在整个操作中拥有全局作用域,所以片段内变量也需要在顶部操作上定义,以便传递到片段上消费。如果一个参数被片段所引用,包含这个片段的操作并没有定义这个变量,那么这个操作将不会被执行。
类型 →
列表类型 → [ 类型 ]
非空类型 →
GraphQL描述查询参数需要的类型为输入类型,可以是某种其他输入类型的列表或者其他输入类型的非空变体。
Semantics/语义
指令 → @名称 参数? 指令为GraphQL文档提供了另一种运行时执行行为和类型验证行为。
有时候,你需要改变GraphQL的执行行为,而参数并不满足要求,譬如条件性的包含或者跳过一个字段。指令通过向执行器描述附加信息来完成这种需求。
指令需要一个名字和一组参数,可以接受任意输入类型。
指令可以用于描述类型、字段、片段、操作的附加信息。
将来版本的GraphQL会加入可配置的执行能力,他们则可能表现为指令。
GraphQL的类型系统用于描述服务器的能力以及判断一个查询是否有效。类系统也描述了查询参数的输入类型,用于运行时检查有效性。
GraphQL服务器的能力是同schema来描述,schema使用其支持的类型和指令来定义。
一个给定GraphQL schema其自身首先必须要通过内部有效性验证,本章节将会讲述这个验证过程的相关规则。
一个GraphQL schema使用每种操作的根级类型表示:query/查询、mutation/更改和subscription/订阅,这表示schema是这三种操作开始的地方。
所有GraphQL schema内的类型都必须要有唯一的名字,任意两个类型都不应该有相同的名字,任意类型也不应该有和内建类型冲突的名字(包含Scalar/标量和Introspection/内省类型)。
所有GraphQL schema内的指令也必须拥有唯一的名字,指令和类型可以拥有相同的名字,因为两者之间并没有歧义。
所有schema内定义的类型和指令都不能以"__"(双下划线)开头命名,因为这是GraphQL内省系统专用。
任何GraphQL Schema的最基本单元都是类型,GraphQL中有8种类型。
最基本的类型是Scalar
/标量,一个标量代表一个原始值,例如字符串或者整数。有时候,一个标量字段的返回值可能是可枚举的,对应这种场景,GraphQL提速了Enum
/枚举类型,其指定了响应结果的有效范围。
标量和枚举型组成了响应结果树的叶子节点,而中间的分支节点则是Object
/对象类型,其定义了一套字段,每个字段是系统中的另一个类型,从而能够定义任意层次的类型层级。
GraphQL支持两种抽象类型:interface/接口和union/联合。
Interface
定义了一系列字段,Object
类型通过实现了其中的字段来实现它。当类型系统表明要返回一个接口时,其返回的都是一个实现这个接口的类型。
Union
定义了一个可能类型的列表,与接口相似,当系统表明要返回一个联合时,其返回的是联合中的一个类型。
这些类型都是可为空且为单数,譬如,一个标量字符串会返回一个null或者一个字符串。通常有需要表示某个类型的列表,于是GraphQL中提供了List
类型,将其他类型封装在其中。类似的Non-Null
类型也是封装其他类型,用以标注返回结果不可为空。这两种类型称为“封装类型”,非封装类型称为基础类型,每个封装类型里面都有一个基础类型,通过不断解封装来找到基础类型。
向GraphQL提供复杂结构作为输入参数是十分有用的,GraphQL为此提供了Input Object
类型,让客户端从schema中获知服务端具体需要什么样的数据。
如名字所示,GraphQL中一个标量代表这一个原始值,GraphQL的响应采用的是树形层级结构,其叶子节点即是标量。
所有GraphQL标量都能以字符串形式表示,虽然取决于所用的返回格式,可能有准确的原始类型来表示指定标量,服务器在适当的时候也应采取这种类型。
GraphQL提供了一些内建标量,类型系统也允许根据语义添加其他标量。假设GraphQL中要定义一个标量Time
/时间,可将字符串转换成ISO‐8601的格式,当查询一个Time
字段时,客户端可以使用ISO‐8601解析器,将这个字段类型转换成客户端特有的原始类型。另一个有潜在用途的标量是Url
,通常会序列化成字符串,但是会由服务器保证是有效的URL。
服务器可能会在schema中省略内建标量,譬如,服务器并未使用浮点数,那么它可能并不会包含Float
类型。 但是一旦schema包含了本规范所述的类型,那么一定会遵守本规范描述的对应行为,譬如,服务器一定不会使用名为Int
的类型去表示128‐bit的数字,或者国际化信息。
Result Coercion/结果类型转换
当GraphQL服务器准备一个标量字段的时候,必须遵守此标量的描述协议,或者强制转换原始值,或者抛出错误。
例如:当服务器在准备一个Int
型标量时,收到的是一个浮点数,如果服务器直接输出这个值,那势必会打破协定,所以服务器可以剔除小数部分,只保留整数部分,然后返回整数值,如果服务器收到的是布尔型值true
,那么就可以返回1
,如果服务器收到的是字符串型,那么就尝试以10为底,解析字符串为整数,如果服务器没法将某些值转换成Int
,那么就只能抛出错误。
因为这个转换行为对于客户端是不可见的,所以准确的转换规则全由实现指定,规范对其的唯一要求就是输出值需遵守标量协议。
Input Coercion/输入类型转换
如果GraphQL的某个参数要求标量作为输入,其类型转换显而易见地必须良好定义。如果输入值无法满足转换规则,则必须抛出错误、
GraphQL对于整数和浮点数输入值有不同的字面量表示方式,其类型转换也对应输入类型做转换。GraphQL可以使用变量作为参数,这些变量的值像是在HTTP的传输中通常会被序列化,由于有些序列化方法并不区分整数和浮点数(譬如JSON),可能因为一个数没有有效小数值而被当作整数而不是浮点数。
下列所有类型中,除了Non‐Null之外,如果显式提供了null,那么其输入结果就会被转换成null。
Built‐in Scalars/内建标量
GraphQL提供一套基本的定义良好的标量类型,每个GraphQL服务器都应该支持这些类型,并且使用这些名字的类型必须遵守下文描述的行为、
整数型标量类型表示一个32位有符号的无小数部分的数值。响应格式应该使用一个支持32位的整数型或者数值类型来表示这个标量类型、
Result Coercion/结果类型转换
GraphQL服务器应该转换非整数型原始数据为整数型,如若不能,则必须抛出字段错误。例如,从浮点型1.0
转换成1
,或者从字符串型"2"
转换成2
。
Input Coercion/输入类型转换
当需要作为输入类型时,只接受整数型输入值。其它类型,包含字符串型数值内容,都要抛出类型不正确的查询错误。如果整数型输入值小于-231或者大于231,也要抛出查询错误。
浮点型标量类型表示一个有符号的双精度小数,见IEEE 754。响应格式应该使用一个合适的双精度数值类型来表示这个标量类型。
Result Coercion/结果类型转换
GraphQL服务器应该转换非浮点型原始数据为型,如若不能,则必须抛出字段错误。例如,从整数型1
转换成1.0
,或者是字符串型"2"
转换成2.0
。
Input Coercion/输入类型转换
当需要作为输入类型时,接受整数型和浮点型输入值。整数型会被转换成小数部分为空的浮点数,譬如整数型输入值1
转换成1.0
,其他类型,包含字符串型数值类型,都要抛出类型不正确的查询错误。如果整型输入值无法使用IEEE 754方式表示,也要抛出查询错误。
字符串型标量表示UTF‐8字符序列组成的文本数据。GraphQL一般使用字符串型来表示任意格式人类可读的文本。所有响应格式都必须支持字符串型表示,字符串型如下所述。
Result Coercion/结果类型转换
GraphQL服务器应该转换非字符串型原始数据为字符串型,如若不能,则必须抛出字段错误。例如,从布尔型true转换成"true"
,从整型1
转换成"1"
。
Input Coercion/输入类型转换
当需要作为输入类型时,只接受有效的UTF‐8字符串型输入值。其它类型都要抛出类型不正确的查询错误。
布尔型标量表示true
或者false
两个值。响应格式应该使用内建的布尔型,如不支持,则使用另外的表示法,整数型1
和0
。
Result Coercion/结果类型转换
GraphQL服务器应该转换非布尔型原始数据为布尔型,如若不能,则必须抛出字段错误。例如,将任意不为零数值转换成true
。
Input Coercion/输入类型转换
当需要作为输入类型时,只接受布尔型输入值。其它类型都要抛出类型不正确的查询错误。
ID型标量表示一个唯一标识符,通常用于重取一个对象或者作为缓存的键。ID型使用String
相同方式来序列化,但是它并不是为了人类可读,虽然它通常可能是数值型,但也总是序列化成String
。
Result Coercion/结果类型转换
GraphQL对ID的格式是不干预的,将其序列化成字符串以保证ID的多种格式之间的相容性,可以是自增数值,可以是128位大数,也可是base64编码后的值和GUID等格式的字符串值。
GraphQL服务器应该将给定ID格式转换成字符串,如若不能,则必须抛出字段错误。
Input Coercion/输入类型转换
当需要作为输入类型时,任意字符串(譬如"4"
)或者整数(譬如4
)都应该被转换成给定服务器支持的ID格式。其他的输入类型,包括浮点型(譬如4.0
)都必须抛出类型不正确的查询错误。
GraphQL查询是层级式的可组装的,以树的形式描述了信息。其中标量类型描述了层级查询中叶子节点的值,对象则描述了中间层。
GraphQL对象表示一个具名字段列表,每个字段会产出一个特定类型的值。对象的值应该向有序映射集(ordered maps),其中查询字段名(或者别名)作为键,字段的结果作为值,以出现在查询中的顺序来排序。
对象中的所有字段都不应以"__"(双下划线)起头命名,因为这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"
}
}
Field Ordering/字段排序
当查询一个对象时,字段的结果映射在概念上的排序顺序应该是跟查询执行期间字段被执行的顺序一致,除了片段上并不适用于当前字段的类型和被@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
}
Result Coercion/结果类型转换
对象类型的结果类型转换判定机制是GraphQL执行器的核心,因此将在本规范的执行器那一章节覆盖这个内容。
Input Coercion/输入类型转换
对象类型不可作为有效输入类型。
概念上的对象字段是会产生值的函数,有时候对象字段能够接受参数来进一步指定返回值。对象字段参数在定义上是一个所有可能参数和参数输入类型的列表。
一个字段内的所有参数都不能以"__"(双下划线)起头命名,因为这GraphQL内省系统专用的命名方式。
例如,Person
拥有一个picture
字段,接受一个参数以返回特定大小的图片链接。
type Person {
name: String
picture(size: Int): Url
}
GraphQL查询可选择性地指定参数,以让字段返回指定参数的结果。
譬如这个案例查询:
{
name
picture(size: 600)
}
可能产生这个结果:
{
"name": "Mark Zuckerberg",
"picture": "http://some.cdn/picture_600.jpg"
}
对象字段的参数可以是任何输入类型。
应用在必要情况下会将对象字段标注为弃用。这样之后,查询弃用字段依然有效(为了保证既有客户端不被这个变更导致异常),但是这种字段应该在文档和工具中正确对待。
对象类型可能因为定义的不严谨而导致潜在的无效性。GraphQL Schema中,以下规则必须被所有对象遵守。
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
,其中NamedEntity
中定义了name
,所以这个查询是有效的。所以,下面这个查询是无效的:
{
entity {
name
age
}
phoneNumber
}
因为entity
指代NamedEntity
,这个接口中并没有定义age
,所以只有在entity
的结果为Person
,查询age
才有效。查询语句可以通过片段或者内联片段来表述这种情况:
{
entity {
name
... on Person {
age
}
},
phoneNumber
}
Result Coercion/结果类型转换
接口类型应该能够判定给定结果对应哪一个对象类型,一旦确定,接口的结果类型转换和对象的接口转换采用一样的方法。
Input Coercion/输入类型转换
接口类型不可作为有效输入类型。
接口类型可能因为定义的不严谨而导致潜在的无效性。
GraphQL联合表示一个对象的类型是对象类型列表中之一,但不保证这些类型之间的字段。另一个区别于接口的方面是,对象会声明其实现的接口,而不知道它被包含的联合。
对于接口和对象,只可以直接查询在其中被定义的字段,如果要查询接口的其他字段,必须使用类型片段。对于联合也是一样,但是联合不定义任何字段,所以联合上不允许查询任何字段,除非使用类型片段。
例如,我们有以下类型系统:
union SearchResult = Photo | Person
type Person {
name: String
age: Int
}
type Photo {
height: Int
width: Int
}
type SearchQuery {
firstSearchResult: SearchResult
}
当查询SearchQuery
类型的firstSearchResult
字段时,查询可能需要片段的所有字段来判断类型。如果结果是Person
,那请求其name
,如果是photo
,那请求height
。下列案例是错的,因为联合上不定义任何字段:
{
firstSearchResult {
name
height
}
}
而正确的查询应该是:
{
firstSearchResult {
... on Person {
name
}
... on Photo {
height
}
}
}
Result Coercion/结果类型转换
联合类型应该能够判定给定结果对应哪一个对象类型,一旦确定,联合的结果类型转换和对象的接口转换采用一样的方法。
Input Coercion/输入类型转换
联合类型不可作为有效输入类型。
联合类型可能因为定义的不严谨而导致潜在的无效性。
GraphQL枚举型是基于标量类型的变体,其表示可能值的一个有限集。
GraphQL枚举型并不指代数值,而是正确的唯一值。他们序列化成字符串,字符串中用名来表示值。
Result Coercion/结果类型转换
GraphQL服务器必须返回定义中可能结果集的一个值。如果无法达成合理的类型转换,则抛出字段错误。
Input Coercion/输入类型转换
GraphQL用常量字面量表示枚举型输入值,GraphQL字符串型字面量不可作为枚举型输入值,否则将抛出查询错误。
查询变量的序列化传输方式中,如果对于非字符串符号值有区别于字符串的表示方法(譬如EDN),那么就应该采用那种方法。否则就像没有这个能力的大多数序列化传输方法一样,字符串型将被转义成同名的枚举型值。
字段上可能会定义参数,客户端将参数合在查询中传输,从而改变字段的行为。这些输入可能是字符串型或者枚举型,但是有时候需要比这个更复杂的类型结构。
上文中定义的Object
并不适合在这儿重用,因为Object
可能包含循环引用或者指代了接口类型或联合类型,这俩都不适合作为输入参数。因此,输入对象才成为了这个系统中单独的类型。
Input Object
定义了输入字段的一个集合,输入字段并不是标量、枚举型或者其他输入对象,这使参数可以接受任意的复杂结构。
Result Coercion/结果类型转换
输入对象类型不可作为有效结果类型。
Input Coercion/输入类型转换
输入对象类型的值应该是输入对象的字面量或者无序映射集,否则将抛出错误。这个无序映射集不应该包含这个输入对象类型中未定义的字段,否则将抛出错误。
如果输入对象上定一个非空字段没有从原始值收到对应条目(譬如变量未赋值,或者赋予了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 | Error: Missing required field b |
{ a: "abc", b: null } | null | Error: b must be non‐null. |
{ a: null, b: 1 } | null | { a: null, b: 1 } |
{ b: $var } | { var: 123 } | { b: 123 } |
{ b: $var } | {} | Error: Missing required field b. |
{ b: $var } | { var: null } | Error: b must be non‐null. |
{ a: $var, b: 1 } | { var: null } | { a: null, b: 1 } |
{ a: $var, b: 1 } | {} | { b: 1 } |
GraphQL列表市一个特殊的集合类型,它声明了列表中元素的类型(下文指代为元素类型)。列表值的序列化结果是一个有序列表,列表中的元素根据元素类型序列化。一个字段使用了一个列表类型,其中封装了元素类型,其标注形式为:pets: [Pet]
。
Result Coercion/结果类型转换
GraphQL服务器必须返回有序列表作为一个列表类型的结果,列表中的每一个元素都是其元素类型的结果类型转换的结果,如果无法达成合理的类型转换,则抛出字段错误。其中,如果返回的是非列表类型,类型转换也会失败,这是要抛出一个类型系统和实现不匹配的异常。
Input Coercion/输入类型转换
当作为输入类型的时候,要求所有列表值都符合列表元素类型。
如果作为输入类型传递给列表类型的值,既不是列表型也不是null空值,那么它将作为列表中的唯一元素作类型转换,这使输入能够在即便声明为“var args”类型参数数组,但只有一个参数被传入(常见场景),客户端可以直接传递值而不用封装成列表。
默认情况下,GraphQL的所有类型都是可空的;null可作为上述所有类型的有效响应。如果要声明一个值不可为空,可以使用GraphQL的非空类型。这个类型封装一个内部类型,并表现为和被封装类型一样的行为,除了null不可作为其有效响应。非空类型使用一个感叹号跟着封装类型的方式来标注:name: String!
。
Nullable vs. Optional/可空 vs 可选
查询上下文中的字段总是可选的,某个字段可以被省略而查询依然有效。然后,非空类型的字段绝不能对查询返回null。
输入(譬如字段参数)默认情况下总是可选的,然而如果一个参数是非空输入类型,那么它将不接受null也不能被省略。为了简明,(nullable)可空类型总是(optional)可选的,(non‐null)非空类型总是(required)必须的。
Result Coercion/结果类型转换
上述所有结果类型转换中,null都被视为有效值,如果转换一个非空类型,那么会使用被封装类型的转换规则。如果非空类型的转换结果不为null空,那么就取这个结果。否则如果结果为null空,那么则抛出字段错误。
Input Coercion/输入类型转换
如果未提供非空类型的输入对象字段或者参数,则被认为是提供了字面量null,或者认为是提供了一个未赋值或者为null空值的运行时变量,然后抛出查询错误。
如果给非空类型提供的值是字面量而非null,或者一个非空变量值,则其会按照被封装类型的输入类型转换规则来转换。
案例:非空参数不可省略。
{
fieldWithNonNullArg
}
案例:null空值不可作为非空参数的值。
{
fieldWithNonNullArg(nonNullArg: null)
}
案例:一个可空的变量不能作为非空参数的值。
query withNullableVariable($var: String) {
fieldWithNonNullArg(nonNullArg: $var)
}
Non‐Null type validation/非空类型验证
一个GraphQL Schema包含了执行引擎支持的指令表。
GraphQL的实现需要提供@skip
和@include
指令。
@skip
指令可用于字段、片段展开以及内联片段,从而能够在执行期间通过if参数完成条件性排除。
下列案例中,experimentalField
只有在$someTest
为false
的时候才会被查询。
query myQuery($someTest: Boolean) {
experimentalField @skip(if: $someTest)
}
@include
指令可用于字段、片段展开以及内联片段,从而能够在执行期间通过if参数完成条件性包含。
下列案例中,experimentalField
只有在$someTest
为true
的时候才会被查询。
query myQuery($someTest: Boolean) {
experimentalField @include(if: $someTest)
}
@skip
和@include
没有优先级差别,当@skip
和@include
同时应用于一个字段时,当且仅当@skip
为false
,@include
为true
的时候它才会被查询。相反的,在仅有@skip
为true
或者仅有@include
为false
的时候它不会被查询。一个GraphQL Schema包含了类型,表示query/查询、mutation/更改和subscription/订阅的起点,提供了进入类型系统的最初入口。其中query类型始终是必须的,并且是一个对象基础类型。mutation类型是可选的,如果其为空,那意味着此系统不支持mutation/更改,如果不为空,它必须是一个对象基础类型。同样的,subscription类型也是可选的,如果其为空,则此系统不支持subscription/订阅,如果不为空,它必须是一个对象基础类型。
query类型的字段表示GraphQL查询中最顶层可用的字段。例如:一个基本的GraphQL查询可能像这样:
query getMe {
me
}
当query对应的类型含有一个名为“me”的字段时,这个查询就是有效的。类似的:
mutation setName {
setName(name: "Zuck") {
newName
}
}
当mutation对应的类型不为空,这个类型有一个名为“setName”参数为“name”的字段时,这个更改就是有效的。
subscription {
newMessage {
text
}
}
当subscription对应的类型不为空,这个类型有一个名为“newMessage”的字段,且只有一个根字段时,这个订阅就是有效的。
一个GraphQL服务器支持基于它的Schema来支持内省,这个Schema可以通过GraphQL自身来查询,创建了一个强大的工具构建平台。
拿一个琐碎的app的样例查询举例。样例中有一个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类型系统作者不能定义任何以双下划线开头的类型、字段、参数和其他类型系统工件。artifact的翻译不确定
所有内省系统中的类型必须提供String
类型的description
字段,以便类型设计者发布文档以增强能力。GraphQL服务可以返回使用Markdown语法(CommonMark中指定)的description
字段。因此建议所有展示description
字段的工具使用CommonMark兼容的Markdown渲染器。
为了支持向后兼容管理,GraphQL字段和枚举值可以指出其是否弃用(isDeprecated: Boolean
)和一个为何弃用的描述(deprecationReason: String
)。
基于GraphQL内省系统建造的工具应该通过隐含信息或者面向开发者的警告信息来减少弃用字段的使用。
GraphQL支持类型命名自行,查询任意对象/接口/联合时可以在一个查询语句的任意位置通过元字段__typename: String!
,得到当前查询的对象类型。
这在查询接口或者联合类型的真实类型的时候用得最为频繁。
这是个隐式字段,并不会出现在定义的类型的字段列表中。
Schema的内省系统可通过查询操作的根级类型上的元字段__schema
和__type
来接入。
__schema: __Schema!
__type(name: String!): __Type
这两个字段也是隐式的,不会出现在查询操作根节点类型的字段列表中。
GraphQL Schema内省系统的Schema:
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
}
__Type
是这个类型内省系统的核心,它代表了这个系统中的标量、接口、对象类型、联合、枚举型。
__Type
也表示类型修改器,其通常用于表示修改一个类型(ofType: __Type
),这正是我们如何表示列表类型和非空类型,以及他们的组合类型。
类型系统中存在多个种类的类型,每种类型都有不同的有效字段,这些类型都被列举在__TypeKind
枚举值中。
标量中譬如整数型、字符串型、布尔型都不能拥有字段。
GraphQL类型设计者需要描述标量的description
字段描述数据格式以及标量的类型转换规则。
字段
kind
必须返回__TypeKind.SCALAR
。name
必须返回字符串。description
必须返回字符串或者null空值。对象类型表示一系列字段的具体实例,内省类型(譬如__Type
,__Field
等)亦是典型的对象类型。
字段
kind
必须返回__TypeKind.OBJECT
。name
必须返回字符串。description
必须返回字符串或者null空值。fields
:本类型上可被查询的字段的集合。includeDeprecated
,默认为false。如过为true则弃用的字段也将返回。interfaces
:对象实现的接口的集合。联合是一个抽象类型,其不声明任何字段。联合的可能类型要显式的在possibleTypes
中列出。类型可不加修改的直接作为联合的一部分。
字段
kind
必须返回__TypeKind.UNION
。name
必须返回字符串。description
必须返回字符串或者null空值。possibleTypes
必须返回联合内可能的类型的列表,它们都必须是对象类型。接口是一个抽象类型,其声明了通用字段。所有实现接口的对象必须定义完全匹配的名字和类型的字段。实现了此接口的类型都有在possibleTypes
中列出。
字段
__TypeKind.INTERFACE
。name
必须返回字符串。description
必须返回字符串或者null空值。fields
: 接口要求的字段的集合。possibleTypes
返回实现了此接口的类型,它们都必须是对象类型。枚举型是特殊的标量,他只能拥有有限集合的标量。
字段
kind
必须返回__TypeKind.ENUM
。name
必须返回字符串。description
必须返回字符串或者null空值。enumValues
:EnumValue
的列表。必须至少有一个值,并且必须有不同的名字。includeDeprecated
,默认为false。如过为true则弃用的字段也将返回。输入对象是复合类型,通常以具名输入值列表的形式作为查询的输入。
例如输入对象Point
可以定义成:
input Point {
x: Int
y: Int
}
字段
kind
必须返回__TypeKind.INPUT_OBJECT
。name
必须返回字符串。description
必须返回字符串或者null空值。inputFields
:InputValue
的列表。列表表示一系列GraphQL值。列表类型是类型修改器:它在ofType
中封装了另一个类型的实例,其定义了列表中的每个元素。
字段
kind
必须返回__TypeKind.LIST
。ofType
:任意类型。GraphQL类型都是可空的,null值是有效的字段类型返回值。
非空类型是类型修改器:它它在ofType
中封装了另一个类型的实例。非空类型不允许null作为返回,也用于表示必要输入参数和必要输入对象字段。
kind
必须返回__TypeKind.NON_NULL
。ofType
:除了Non‐null外的所有类型。列表和非空可以通过组合以表示更为复杂的类型。
如果列表修改的类型是非空,那么列表不能包含任何null空值元素。
如果非空修改的类型是列表,那么不能接受null空值,但是空数组可接受。
如果列表修改的类型是列表,那么第一个列表的元素是第二个列表类型的列表。
非空类型不能修改另一个非空类型。
__Field
类型表示对象或者接口类型的每一个字段。
字段
name
必须返回字符串。description
必须返回字符串或者null空值。args
返回__InputValue
的列表,表示这个字段接受的参数。type
必须返回__Type
,表示这个字段值的类型。isDeprecated
返回true如果这个字段不应再被使用,否则false。deprecationReason
可选,提供字段被弃用的原因。__InputValue
类型表示字段和指令的参数,如同输入对象的inputFields
字段。
字段
name
必须返回字符串。description
必须返回字符串或者null空值。type
必须返回表示这个输入值期待的类型的__Type
。defaultValue
返回一个(使用GraphQL语言)字符串,表示这个输入值在运行时未提供值的情况下的默认值,如果这个输入值没有默认值,返回null空值。__EnumValue
表示枚举型的可能值之一。
字段
name
必须返回字符串。description
必须返回字符串或者null空值。isDeprecated
返回true如果这个字段不应再被使用,否则false。deprecationReason
可选,提供字段被弃用的原因。__Directive
类型表示服务器支持的一个指令。
字段
name
必须返回字符串。description
必须返回字符串或者null空值。locations
返回__DirectiveLocation
列表,表示指令可以放置的有效位置。args
返回__InputValue
的列表,表示这个字段接受的参数。 GraphQL不仅会检测一个请求是否句法上正确,还会其在给定GraphQL schema上下文内无歧义无错误。
一个无效请求依然是技术上可执行的,也能通过执行章节的步骤产生稳定的结果,但是相对于这个有验证错误的请求,其结果可能是有歧义的、意外不可预知的,所以对于无效请求,不应该予以执行。
典型的验证会在请求执行前的上下文中执行,但是如果给定请求之前已经通过验证,那么GraphQL服务在执行这个请求前可能不会显式的验证它,譬如一个请求在开发期已经通过验证,并假设他后面不会改变,或者服务器层验证了一个请求,记住了它的验证结果以避免后续再次验证同样的请求。因此任何客户端或者开发期工具,都应该汇报验证错误,并阻止构建或者执行当时已知错误的请求。
Type system evolution/类型系统演变
GraphQL类型系统可能会随着时间添加一些类型和字段,从而发生了演变,之前有效的请求之后可能就变得无效。任何让之前有效的请求变得无效的变化都称之为破坏性变化。GraphQL服务和schema维护者被鼓励要避免破坏性变化,但是为了保证面对破坏性变化的弹性,复杂的GraphQL系统可能依然允许执行在某个点上没有错误也未曾改变的请求。
Examples/案例
至于本章节的schema,我们假定有如下类型系统用于描述案例:
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
}
Formal Specification/形式规范
Explanatory Text/解释文本
每一个具名操作定义必须是在其文档中唯一,以便于使用其名字指代。
例如下列文档就是有效的:
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
}
}
Formal Specification/形式规范
Explanatory Text/解释文本
GraphQL允许在文档只有一个操作存在时用简写形式定义查询操作。
例如下列文档就是有效的:
{
dog {
name
}
}
然而这个文档是无效的:
{
dog {
name
}
}
query getName {
dog {
owner {
name
}
}
}
Formal Specification/形式规范
Explanatory Text/解释文本
订阅操作必须只有一个根字段。
有效案例:
subscription sub {
newMessage {
body
sender
}
}
fragment newMessageFields on Message {
body
sender
}
subscription sub {
newMessage {
... newMessageFields
}
}
无效案例:
subscription sub {
newMessage {
body
sender
}
disallowedSecondRootField
}
内省字段也是计算在内的,以下案例是无效的:
subscription sub {
newMessage {
body
sender
}
__typename
}
Formal Specification/形式规范
Explanatory Text/解释文本
字段选择的目标字段必须定义在选择集的范围类型上。对别名没有限制。
譬如下列片段无法通过验证:
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
}
Formal Specification/形式规范
FieldsInSetCanMerge(set):
SameResponseShape(fieldA, fieldB):
Explanatory Text/解释文本
如果执行期间遇到了相同响应名的多个字段选择,执行的字段和参数以及结果值都应该避免歧义。然而,任意两个字段选择能在同一个对象里面遇到还是有效的,那只能是它们等价的情况。
对于简单的手写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
}
}
Formal Specification/形式规范
Explanatory Text/解释文本
标量上的字段选择是不允许的,标量在任何GraphQL查询的都只是叶子节点。
下列是有效的。
fragment scalarSelection on Dog {
barkVolume
}
下列是无效的。
fragment scalarSelectionsNotAllowedOnBoolean on Dog {
barkVolume {
sinceWhen
}
}
相反的,GraphQL查询的叶子字段选择必须为标量。在接口、联合或者对象上的选择也不允许没有下级选择。
假设schema有下列查询根类型的补充内容:
extend type QueryRoot {
human: Human
pet: Pet
catOrDog: CatOrDog
}
以下案例是无效的
query directQueryOnObjectWithoutSubFields {
human
}
query directQueryOnInterfaceWithoutSubFields {
pet
}
query directQueryOnUnionWithoutSubFields {
catOrDog
}
参数在字段和指令上都有使用,下列验证规则可应用于这两种情况。
Formal Specification/形式规范
Explanatory Text/解释文本
提供给字段或者指令的每一个参数,必须在字段或者指令的可能参数集合中定义。
譬如下列是有效的:
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)
}
字段和指令将参数视作参数名到从参数值的映射,一个参数集合内有多于一个参数拥有一个样的参数名时将会产生歧义,也是无效的。
Formal Specification/形式规范
Formal Specification/形式规范
Explanatory Text/解释文本
字面量值必须和使用它的参数的类型相兼容,兼容规则为类型系统章节所定义的转换规则。
譬如,一个Int型可以被转换为Float型。
fragment goodBooleanArg on Arguments {
booleanArgField(booleanArg: true)
}
fragment coercedIntIntoFloatArg on Arguments {
floatArgField(floatArg: 1)
}
而String到Int则是不可转换的,因此下面的案例是无效的:
fragment stringIntoInt on Arguments {
intArgField(intArg: "3")
}
Explanatory Text/解释文本
参数也能是必要的,只要参数的类型是非空,那么这个参数就是必须的,且显式值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)
}
Formal Specification/形式规范
Explanatory Text/解释文本
片段定义在片段解构中使用名字引用。为避免歧义,每一个片段都必须是片段内唯一。
行内片段并不被当作片段定义,也不被这些验证规则影响。
例如以下文档是有效的:
{
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
}
}
Formal Specification/形式规范
Explanatory Text/解释文本
片段必须在schema中存在的类型上指定,这同时适用于具名片段和内联片段。如果它们不存在于schema上,那么这个查询则无法通过验证。
譬如下列片段是有效的:
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
}
}
Formal Specification/形式规范
Explanatory Text/解释文本
片段只能在联合、接口和对象上声明,在标量上是无效的,只能应用与非叶子节点,这个规则同时适用于具名片段和行内片段。
下列片段声明是有效的:
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
}
}
Formal Specification/形式规范
Explanatory Text/解释文本
已定义的片段必须在查询文档中使用。
例如下列是一个无效的查询文档:
fragment nameFragment on Dog { # unused
name
}
{
dog {
name
}
}
字段选择也被片段解构之间的互相调用决定。譬如目标片段的选择集和同级的引用目标片段相联合。
Formal Specification/形式规范
Explanatory Text/解释文本
具名片段解构必须指定文档中已经定义的片段。如果结构的目标没有定义,则将会报错:
{
dog {
...undefinedFragment
}
}
Formal Specification/形式规范
DetectCycles(fragmentDefinition, visited):
Explanatory Text/解释文本
片段结构的图不可造成任何循环,即便是对自身的解构。否则一个操作会下层数据的环上无尽地结构无尽地执行。
这使可能导致无尽解构的片段失效;
{
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
}
}
Formal Specification/形式规范
GetPossibleTypes(type):
Explanatory Text/解释文本
片段在一个类型上声明,并只在这个运行时对象匹配类型条件的时候应用。它们也能在父类型的上下文中解构。片段解构只有在类型条件能够应用与父类型的时候有效。
在一个对象类型范围内,唯一有效的对象类型片段解构是能够应用与范围内同一类型的(片段)。
例如:
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的类型。
Formal Specification/形式规范
Explanatory Text/解释文本
输入对象不能包含多余一个同名的字段,否则存在让部分句法被忽略的歧义。
例如,下列查询并不会通过验证。
{
field(arg: { field: true, field: false })
}
Formal Specification/形式规范
Explanatory Text/解释文本
GraphQL服务器定义他们支持的指令,对于每个指令的使用,必须要指令在服务器上可用。
Formal Specification/形式规范
Explanatory Text/解释文本
GraphQL定义了他们支持的指令,在何处支持。每一指令的使用,都必须在服务器声明支持使用的地方。
譬如,下列查询无法通过验证,因为@skip
在QUERY
上,并不是有效位置。
query @skip(if: $foo) {
field
}
Formal Specification/形式规范
Explanatory Text/解释文本
指令用于描述其应用的定义的元数据或者表现的改变。当同名的指令多次使用时,期望的源数据或者表现就会变得有歧义,因此一个位置上只允许使用一个指令。
譬如,下列查询不会通过验证,因为@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
}
}
Formal Specification/形式规范
Explanatory Text/解释文本
任意操作定了多于一个同名变量,将会变得有歧义,并是无效的。即便重复变量的值是一样的,他也是无效的。
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)
}
}
Formal Specification/形式规范
Explanatory Text/解释文本
操作中定一个变量都是可以定义默认值的,如果对应类型不是non‐null。
例如,下列查询能通过验证。
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)
}
}
Formal Specification/形式规范
Explanatory Text/解释文本
变量只能是标量、枚举型和输入对象,或者这些类型的列表和非空封装变体,这些是输入类型。而对象、联合、接口不能作为输入。
对于这些的案例,假设有如下类型系统补充内容:
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) {
# ...
}
Formal Specification/形式规范
Explanatory Text/解释文本
变量的有效范围是在每一个操作内的,也就是说任何在操作上下文中要使用的变量,必须在那个操作的顶部预先定义。
例如:
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操作引入使用。
Formal Specification/形式规范
Explanatory Text/解释文本
操作上定义的所有变量必须至少被使用一次,无论是操作本身使用,还是操作引入的片段通过传递引用使用。未使用的变量会导致验证错误。
例如,下列是无效的:
query variableUnused($atOtherHomes: Boolean) {
dog {
isHousetrained
}
}
因为$atOtherHomes没有被引用过。
这个规则也适用于片段解构的传递引用:
query variableUsedInFragment($atOtherHomes: Boolean) {
dog {
...isHousetrainedFragment
}
}
fragment isHousetrainedFragment on Dog {
isHousetrained(atOtherHomes: $atOtherHomes)
}
上面是有效的,因为$atOtherHomes在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定义了额外的一个变量。
Formal Specification/形式规范
Explanatory Text/解释文本
变量使用必须和它们传递进去的参数兼容。
验证失败会发生在变量在类型上下文完全不匹配和传递可空类型参数给非空类型变量的时候。
类型必须匹配:
query intCannotGoIntoBoolean($intArg: Int) {
arguments {
booleanArgField(booleanArg: $intArg)
}
}
$intArg是Int类型,不能作为参数传递给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
/文档(本规范的“Query Language”部分有定义过),如果文档中定义了多个操作还需要有选择的操作名,不然文档将被视为只包含一个操作。请求的结果取决于其操作根据下文“Executing Operations”一节执行的结果。
ExecuteRequest(schema, document, operationName, variableValues, initialValue):
GetOperation(document, operationName):
如同验证章节中解释的一样,只有通过了所有验证的请求才应该被执行。如果得到了验证错误,它们将被添加到响应中的“errors”列表中,而这个请求也就不执行直接失败。
典型的情况下,验证将在请求执行之前的上下文内瞬间完成,但是在一个相同请求之前已经验证过的情况下,GraphQL服务可能直接执行而不验证。GraphQL服务只应该执行那些某个时间点上没有验证错误也没更改过的请求。
例如:一个请求在开发期已经通过验证,并假设他后面不会改变,或者服务器层验证了一个请求,记住了它的验证结果以避免后续再次验证同样的请求。
如果操作定义了任何变量,然后这些变量的值需要根据变量声明的类型的输入转换规则而转换。如果变量值的输入转换中发生了查询错误,操作会不执行直接失败。
CoerceVariableValues(schema, operation, variableValues):
如果本规范的“Type System”/类型系统一章节所描述,类型系统必须提供一个查询的根级对象类型。如果支持更改或者订阅,它也必须提供更改或者订阅对应的根级对象类型。
如果操作是一个查询,那操作的结果就是用查询根级对象类型执行查询的顶层选择集的结果。
执行一个查询的时候可以提供一个初始值。
ExecuteQuery(query, schema, variableValues, initialValue):
如果操作是一个更改,那操作的结果就是用更改根级对象类型执行更改的顶层选择集的结果。这个选择集应该依次执行。
更改的顶层字段被期望用于在下层数据系统上执行副作用操作。依次执行这些更改,以保证副作用操作期间没有竞态条件。
ExecuteMutation(mutation, schema, variableValues, initialValue):
如果操作是一个订阅,那结果是一个事件流,称作“Response Stream”响应流,事件流中的每一个事件即是针对下层“Source Stream”源流上的新事件执行操作的结果。
执行订阅会在服务端创建一个永久的函数,用于将下层源流映射成返回的响应流。
Subscribe(subscription, schema, variableValues, initialValue):
考虑一个聊天应用案例。客户端发送一个如下请求来订阅投递到聊天室的新消息:
subscription NewMessages {
newMessage(roomId: 123) {
sender
text
}
}
当客户端订阅后,任何时候有ID为“123”的新消息投递到聊天室后,对于“sender”和“text”的选择将会被执行然后发布到客户端。例如:
{
"data": {
"newMessage": {
"sender": "Hagrid",
"text": "You're a wizard!"
}
}
}
”新消息投递到聊天室”可以使用了“Pub‐Sub”发布订阅系统,其中聊天室ID是“topic”,并且每一个“publish”都包含了“sender”发送者和“text”文本。
Event Streams
事件流表示一序列可观测的离散事件。例如,一个“Pub‐Sub”系统当“subscribing to a topic”订阅了一个话题可能产生一个事件流,每一次“publish”发布到话题都会在事件流上产生一个事件。事件流可能产生一序列无尽的事件,也可能在任何时候终止。响应中的事件流可能因为发生错误而终止,也可能仅仅因为没有后续事件发生。观察者可以在任何时候取消订阅而停止观察事件流,这之后就不会从事件流中收到后续事件了。
Supporting Subscriptions at Scale/支持大规模订阅
支持订阅对GraphQL服务器而言是一个显著的变化。查询和更改操作是无状态的,可以通过克隆GraphQL服务器实例而扩展规模。订阅却相反,它是有状态的,要求在这个订阅的生命周期内维持文档、变量和其他上下文。
考虑下你服务中某个机器宕机状态丢失的时候,你的系统的行为。使用分离的专用服务器来处理订阅状态和客户端连接将能提升系统的持久性和可用性。
源流表示会触发GraphQL对应事件执行的一序列事件。就像字段值的解析一样,创建源流的逻辑也是应用特定的。
CreateSourceEventStream(subscription, schema, variableValues, initialValue):
ResolveFieldEventStream(subscriptionType, rootValue, fieldName, argumentValues):
下层源流中的每一个事件都会触发订阅使用这个事件作为根值执行选择集。
MapSourceToResponseEvent(sourceStream, subscription, schema, variableValues):
ExecuteSubscriptionEvent(subscription, schema, variableValues, initialValue):
当客户端不再想要收到订阅的载荷时可以通过退订来取消响应流。这可能也同时取消掉了源流。这个是一个清理被这个订阅占用的其他资源的好机会。
Unsubscribe(responseStream)
要执行选择集,对象值必须得到,对象类型必须已知,同样还需要知道其需要依次执行还是并列执行。
首先,选择集被转换成分组的字段集合,然后分组字段集合内的每一个字段都会在响应映射集中产生一个条目。
ExecuteSelectionSet(selectionSet, objectType, objectValue, variableValues):
正常情况下,不论分组字段集合中的条目顺序为何,执行器都能执行(通常是并行执行)。因为除了顶层更改必然有副作用且具有幂等性,字段解析的执行顺序必然不会影响其结果,因此服务器能够以他认为优化的方式自由地执行字段条目。
例如,有下正常执行的分组字段:
{
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 }
次级选择集。举一个说明性的例子,假设我们有一个mutation字段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()生成的字段分组的深度优先搜索通过执行来保持,保证字段在执行后响应中以稳定可预测的顺序出现。
CollectFields(objectType, selectionSet, variableValues, visitedFragments):
@skip
指令,使skipDirective为此指令。@include
指令,使includeDirective为此指令。DoesFragmentTypeApply(objectType, fragmentType):
分组字段集上的每一个请求字段(定义在被选择的对象类型上)都会得到一个响应映射集中的一个条目。字段执行首先会转换任何提供的技术值,然后解析这个字段的值,最后通过递归执行另一个选择集或者转换一个标量来完成这个字段的值。
ExecuteField(objectType, objectValue, fieldType, fields, variableValues):
字段可能包含下层运行时产生正确结果的参数,这些参数定义在类型系统中的字段上,都有一个特定的输入类型:Scalars标量、Enum庙举行、Input Object输入对象,或者这三个的List列表和Non‐Null非空封装。
对于查询中每个参数的位置,可能是一个字面量值,也可能是运行时提供的变量。
CoerceArgumentValues(objectType, field, variableValues):
虽然几乎所有的GraphQL执行都能通用化描述,但最终内部系统暴露给GraphQL接口的时候必须提供值。这通过ResolveFieldValue暴露,其生成真实值的类型上给定字段的值,
在案例中,这个可能接收objectType/对象类型Person
,其field/字段为"soulMate",这个objectValue/类型值表示John Lennon。它可能被期待得到值表示Yoko Ono。
ResolveFieldValue(objectType, objectValue, fieldName, argumentValues):
在解析一个字段的值后,再确认其符合期望返回类型即完成。如果返回值是另一个对象类型,然后字段执行将继续递归。
CompleteValue(fieldType, fields, result, variableValues):
Resolving Abstract Types/解析抽象类型
当完成一个具有抽象返回类型的字段时,譬如Interface接口或者Union联合返回类型,首先,抽象类型必须接地到一个相关的对象类型上去,这个由内部系统决定哪一个是合适的。
ResolveAbstractType(abstractType, objectValue):
Merging Selection Sets/合并选择集
当多余一个同名字段并行执行后,他们的选择集在完成这个值的时候被合并,以继续次级选择集的执行。
案例查询中,描述了带次级选择集的同名的并行字段。
{
me {
firstName
}
me {
lastName
}
}
当解析了 me
的值后,选择集将合并,所以firstName
和lastName
可以被解析到一个值上。
MergeSelectionSets(fields):
当解析一个字段时抛出了错误,它应该被当作这个字段返回了null,且错误必须添加到响应的"errors"列表中。
如果一个字段解析的结果就是null(不论是字段的解析函数返回了null还是发生了错误),且那个字段是Non-Null
,那么抛出一个字段错误。这个错误必须添加到响应的"errors"列表中。
如果字段因为错误而返回了null,这个错误也被添加到了响应的"errors"列表中,这个"errors"列表后续就不应被影响,即是,错误列表中一个字段上只允许添加一个错误。
因为Non-Null
类型不能为null,字段错误将会冒泡到父级字段并被父级字段处理。如果父级字段可能为null,那么它就解析为null,否则如果父级字段也是Non-Null
类型,那么这个字段错误将会继续冒泡到上一级父级字段。
如果从请求的根到错误的源的所有字段都返回Non-Null
条目,那么响应中的"data"条目应该为null。
如果一个GraphQL服务器收到一个请求,它必须返回一个良好格式化的响应。如果请求操作执行成功,服务器的响应则描述其执行结果,否则描述执行期间遇到的错误。
一个响应可能会包含部分是响应,部分是另一个字段上发生错误的时候,响应中这个字段的值被替换成了null。
GraphQL并不要求特定的序列化格式,然而客户端应该使用一种支持GraphQL响应中的主要原始类型的序列化格式。特别需要支持一下原始类型:
在执行一节中的CollectFields()的定义中,序列化格式如果支持有序映射集,那么它应该保持请求字段的顺序。只支持无序映射集的序列化格式(譬如JSON)应该保持其语法上的顺序。
产生跟请求一致的字段顺序响应有助于提过调试过程中面向人类的可读性和属性相关的响应解析效率。
系列化格式应支持下列原始类型,然而String可能用于替换下列原始类型。
虽然如上所述,GraphQL并不要求一种特定的序列化格式,但是还是比较偏好于JSON。为了风格统一和易于标注,本规范中响应的案例都以JSOn的格式表示,特别需要注意的是,我们的JSON案例中,原始类型都以一下JSON概念来表示:
GraphQL值 | JSON值 |
---|---|
Map | Object |
List | Array |
Null | null |
String | String |
Boolean | true or false |
Int | Number |
Float | Number |
Enum Value | String |
Object Property Ordering/对象属性排序
JSON被描述为无序键值对集合,而键值对在表示上确实有序的方式。换言之,即便JSON字符串{ "name": "Mark", "age": 30 }
和{ "age": 30, "name": "Mark" }
编码了相同的值,但是他们却有不同的属性表示顺序。
选择集的求值结果是有序的,这个顺序来源于请求,并由查询执行器定义,因此JSON序列化可以保持这个顺序,并以这个顺序写入到对象属性中。
譬如,如果查询是{ name, age }
,则GraphQL服务器响应的JSON会是{ "name": "Mark", "age": 30 }
而不是{ "age": 30, "name": "Mark" }
。
GraphQL的响应应该是映射集类型。
如果操作包含了执行,那其响应映射集的第一个条目的键必须是data
,其值将在“Data/数据”一章节中描述。如果操作在执行之前就失败,譬如语法错误、缺失信息或者验证错误,则此条目不应显示。
如果操作遇到了错误,则响应映射集的下一个条目的键必须是errors
,其值将在“Errors/错误”一章节中描述。如果操作并没遇到错误,则此条目不应显示。
响应可以包含键为extensions
的条目,此条目必须包含值。此条目是作为协议扩展而为实现者保留的,因此并没有对其内容的附加限制要求。
为了保证本协议今后的变化不会破坏已有的服务器和客户端,顶层的响应映射集不能包含上述三个之外的条目。
响应中的data
条目是请求的操作执行的结果。如果操作是query/查询,输出则是schema查询的根级类型对象,如果操作是mutation/更改,输出则是schema更改的根级类型对象。
如果在执行前遇到错误,结果中将不应有data
条目。
如果在执行中遇到错误,并导致不能返回有效响应,则data
条目应该为null
。
响应中的errors
是一个非空错误列表,每个错误是一个映射集。
如果在执行请求的操作中未遇到错误,结果中将不应有errors
条目。
如果响应结果中没有data
条目,则响应中的errors
条目不可为空,其必须包含至少一条错误,这个错误应指出为什么没有数据返回。
如果响应中包含data
条目(包含值为null的情况),那么响应中的errors
条目可以包含执行期间的任何错误。如果执行期发生了错误,那这个错误就应该被包含。
Error result format/错误结果格式
每个错误都必须包含键为message
的条目,其包含了针对开发这错误描述,以便修正错误。
如果一个错误能和请求的GraphQL文档特定点所匹配,它应该包含键为locations
的条目,其内容为一个定位列表,每个定位都是键为line
和column
的映射集,两者都是从1
开始的正数,用以描述相关的语法元素的起始位置。
如果一个错误能和GraphQL结果中的特定字段关联,它必须包含键为path
的条目,其描述了响应中哪一个字段面临了错误。这能让客户端鉴别一个null
是正常逻辑还是运行时错误。
这个字段应该是一个路径段的列表,从响应根级开始直到关联的字段结束。用于表示字段的路径段应该是字符串类型,而表示列表索引的路径段则应该是从0开始的整数。如果错误发生在别名字段,那对应的路径应该使用别名,因为其表示的是响应中的路径而非查询中的。
例如,如果下列查询中,获取一个朋友的名字失败了:
{
hero(episode: $episode) {
name
heroFriends: friends {
id
name
}
}
}
响应会像这样:
{
"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
字段在schema中声明为Non-Null
非空,那么查询结果可能不同,但是错误还是一样。
{
"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服务器可以给error提供附加的条目,以用来展示更为用的或者机器可读的错误,当然,今后本规范也可能为error引入附加条目。
This specification document contains a number of notation conventions used to describe technical concepts such as language grammar and semantics as well as runtime algorithms.
This appendix seeks to explain these notations in greater detail to avoid ambiguity.
A context‐free grammar consists of a number of productions. Each production has an abstract symbol called a “non‐terminal” as its left‐hand side, and zero or more possible sequences of non‐terminal symbols and or terminal characters as its right‐hand side.
Starting from a single goal non‐terminal symbol, a context‐free grammar describes a language: the set of possible sequences of characters that can be described by repeatedly replacing any non‐terminal in the goal sequence with one of the sequences it is defined by, until all non‐terminal symbols have been replaced by terminal characters.
Terminals are represented in this document in a monospace font in two forms: a specific Unicode character or sequence of Unicode characters (ex. = or terminal), and a pattern of Unicode characters defined by a regular expression (ex /[0-9]+/).
Non‐terminal production rules are represented in this document using the following notation for a non‐terminal with a single definition:
While using the following notation for a production with a list of definitions:
A definition may refer to itself, which describes repetitive sequences, for example:
The GraphQL language is defined in a syntactic grammar where terminal symbols are tokens. Tokens are defined in a lexical grammar which matches patterns of source characters. The result of parsing a sequence of source Unicode characters produces a GraphQL AST.
A Lexical grammar production describes non‐terminal “tokens” by patterns of terminal Unicode characters. No “whitespace” or other ignored characters may appear between any terminal Unicode characters in the lexical grammar production. A lexical grammar production is distinguished by a two colon ::
definition.
A Syntactical grammar production describes non‐terminal “rules” by patterns of terminal Tokens. Whitespace and other ignored characters may appear before or after any terminal Token. A syntactical grammar production is distinguished by a one colon :
definition.
This specification uses some additional notation to describe common patterns, such as optional or repeated patterns, or parameterized alterations of the definition of a non‐terminal. This section explains these short‐hand notations and their expanded definitions in the context‐free grammar.
Constraints
A grammar production may specify that certain expansions are not permitted by using the phrase “but not” and then indicating the expansions to be excluded.
For example, the production:
means that the nonterminal SafeName may be replaced by any sequence of characters that could replace Name provided that the same sequence of characters could not replace SevenCarlinWords.
A grammar may also list a number of restrictions after “but not” separated by “or”.
For example:
Optionality and Lists
A subscript suffix “Symbolopt” is shorthand for two possible sequences, one including that symbol and one excluding it.
As an example:
is shorthand for
A subscript suffix “Symbollist” is shorthand for a list of one or more of that symbol.
As an example:
is shorthand for
Parameterized Grammar Productions
A symbol definition subscript suffix parameter in braces “SymbolParam” is shorthand for two symbol definitions, one appended with that parameter name, the other without. The same subscript suffix on a symbol is shorthand for that variant of the definition. If the parameter starts with “?”, that form of the symbol is used if in a symbol definition with the same parameter. Some possible sequences can be included or excluded conditionally when respectively prefixed with “[+Param]” and “[~Param]”.
As an example:
is shorthand for
This specification describes the semantic value of many grammar productions in the form of a list of algorithmic steps.
For example, this describes how a parser should interpret a string literal:
This specification describes some algorithms used by the static and runtime semantics, they’re defined in the form of a function‐like syntax along with a list of algorithmic steps to take.
For example, this describes if a fragment should be spread into place given a runtime objectType and the fragment’s fragmentType:
! | $ | ( | ) | ... | : | = | @ | [ | ] | { | | | } |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
+ | - |
" | \ | / | b | f | n | r | t |
query | mutation | subscription |
true | false |