GraphQL: 検証(validation)

本稿は公式サイト「[Validation](https://graphql.org/learn/validation/)」にもとづく、GraphQLの構文検証の考え方についての解説です。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。なお、GraphQL公式サイトのコード例は、インタラクティブな環境です。コードを書き替えて結果が確かめられますので、ぜひ試してみてください。

型システムを用いることにより、GraphQLクエリが有効かどうかあらかじめ判断できます。無効なクエリがつくられたとき、実行時のチェックに頼ることなく、サーバーとクライアントは効果的に開発者に知らせられるのです。

スターウォーズの例については、GitHubの[`graphql-js/src/__tests__/starWarsValidation-test.ts`](https://github.com/graphql/graphql-js/blob/main/src/__tests__/starWarsValidation-test.ts)に、無効かどうかを試すさまざまなクエリが含まれています。これは、参照の実装を検証できているか確かめるテストファイルです。

まずは、複雑でも有効なクエリを採り上げましょう。ネストされたクエリで、これまでご紹介したコード例と大きくは変わりません。ただし、重複するフィールドは、フラグメントに分けました。

```graphql:GraphQL
{
	hero {
		...NameAndAppearances
		friends {
			...NameAndAppearances
			friends {
				...NameAndAppearances
			}
		}
	}
}

fragment NameAndAppearances on Character {
	name
	appearsIn
}
```

```json:結果
{
	"data": {
		"hero": {
			"name": "R2-D2",
			"appearsIn": [
				"NEWHOPE",
				"EMPIRE",
				"JEDI"
			],
			"friends": [
				{
					"name": "Luke Skywalker",
					"appearsIn": [
						"NEWHOPE",
						"EMPIRE",
						"JEDI"
					],
					"friends": [
						{
							"name": "Han Solo",
							"appearsIn": [
								"NEWHOPE",
								"EMPIRE",
								"JEDI"
							]
						},
						{
							"name": "Leia Organa",
							"appearsIn": [
								"NEWHOPE",
								"EMPIRE",
								"JEDI"
							]
						},
						{
							"name": "C-3PO",
							"appearsIn": [
								"NEWHOPE",
								"EMPIRE",
								"JEDI"
							]
						},
						{
							"name": "R2-D2",
							"appearsIn": [
								"NEWHOPE",
								"EMPIRE",
								"JEDI"
							]
						}
					]
				},
				{
					"name": "Han Solo",
					"appearsIn": [
						"NEWHOPE",
						"EMPIRE",
						"JEDI"
					],
					"friends": [
						{
							"name": "Luke Skywalker",
							"appearsIn": [
								"NEWHOPE",
								"EMPIRE",
								"JEDI"
							]
						},
						{
							"name": "Leia Organa",
							"appearsIn": [
								"NEWHOPE",
								"EMPIRE",
								"JEDI"
							]
						},
						{
							"name": "R2-D2",
							"appearsIn": [
								"NEWHOPE",
								"EMPIRE",
								"JEDI"
							]
						}
					]
				},
				{
					"name": "Leia Organa",
					"appearsIn": [
						"NEWHOPE",
						"EMPIRE",
						"JEDI"
					],
					"friends": [
						{
							"name": "Luke Skywalker",
							"appearsIn": [
								"NEWHOPE",
								"EMPIRE",
								"JEDI"
							]
						},
						{
							"name": "Han Solo",
							"appearsIn": [
								"NEWHOPE",
								"EMPIRE",
								"JEDI"
							]
						},
						{
							"name": "C-3PO",
							"appearsIn": [
								"NEWHOPE",
								"EMPIRE",
								"JEDI"
							]
						},
						{
							"name": "R2-D2",
							"appearsIn": [
								"NEWHOPE",
								"EMPIRE",
								"JEDI"
							]
						}
					]
				}
			]
		}
	}
}
```

随分と長い結果が帰りました。それでもこのクエリは有効です。つぎに、無効なクエリを見てみましょう。

フラグメントは自らを参照したり、循環をつくることはできません。結果が無限ループしてしまうかもしれないからです。上のコード例のクエリでは、フラグメントが3つ入れ子でした。この入れ子をなくします。

```graphql:GraphQL
{
	hero {
		...NameAndAppearancesAndFriends
	}
}

fragment NameAndAppearancesAndFriends on Character {
	name
	appearsIn
	friends {
		...NameAndAppearancesAndFriends
	}
}
```

```json:結果
{
	"errors": [
		{
			"message": "Cannot spread fragment \"NameAndAppearancesAndFriends\" within itself.",
			"locations": [
				{
					"line": 11,
					"column": 5
				}
			]
		}
	]
}
```

このエラー(`errors`)のメッセージ(`message`)が告げるのは、フラグメント`NameAndAppearancesAndFriends`の中で自身のフラグメントは展開できないということです。

> Cannot spread fragment "NameAndAppearancesAndFriends" within itself.

また、フィールドを問い合わせるときは、その型に存在するフィールドを要求しなければなりません。`hero`は`Character`を返すのですから、問い合わせられるのは`Character`がもつフィールドです。けれど、型の中に`favoriteSpaceship`というフィールドはありません。したがって、つぎのクエリも無効です。

```graphql:GraphQL
# 無効: `favoriteSpaceship`は`Character`にない
{
	hero {
		favoriteSpaceship
	}
}
```

```json:結果
{
	"errors": [
		{
			"message": "Cannot query field \"favoriteSpaceship\" on type \"Character\".",
			"locations": [
				{
					"line": 4,
					"column": 5
				}
			]
		}
	]
}
```

エラー(`errors`)メッセージ(`message`)も、型`Character`にフィールド`favoriteSpaceship`は要求できないと告げています。

> Cannot query field "favoriteSpaceship" on type "Character".

今度の無効なコード例は、フィールドを問い合わせたときにスカラーか列挙型以外が返ってくる場合です。そのフィールドからどういうデータを得たいのか、必ず指定しなければなりません。`hero`は`Character`を返し、`name`と`appearsIn`のフィールドが含まれています(「GraphQL: スキーマと型」の「オブジェクト型とフィールド」参照)。具体的なフィールドを省いてしまえば、クエリは有効でなくなるのです。

```graphql:GraphQL
# INVALID: heroはスカラーではないのでフィールドが必要
{
	hero
}
```

```json:結果
{
	"errors": [
		{
			"message": "Field \"hero\" of type \"Character\" must have a selection of subfields. Did you mean \"hero { ... }\"?",
			"locations": [
				{
					"line": 3,
					"column": 3
				}
			]
		}
	]
}
```

エラー(`errors`)メッセージ(`message`)は、フィールド`hero`は`Character`型なので、サブフィールドを指定するよう求めます。

> Field "hero" of type "Character" must have a selection of subfields. Did you mean "hero { ... }"?

逆に、スカラーのフィールドは、その中にフィールドを含みません。フィールドを要求すれば、クエリは無効です。

```graphql:GraphQL
# 無効: nameはスカラーなのでフィールドは要求できない
{
	hero {
		name {
			firstCharacterOfName
		}
	}
}
```

```json:結果
{
	"errors": [
		{
			"message": "Field \"name\" must not have a selection since type \"String!\" has no subfields.",
			"locations": [
				{
					"line": 4,
					"column": 10
				}
			]
		}
	]
}
```

エラー(`errors`)メッセージ(`message`)は、フィールド`name`にはサブフィールドがないことを示します。

> Field "name" must not have a selection since type "String!" has no subfields.

前述のとおり、クエリは対象となる型に備わるフィールドのみ問い合わせできます。`Character`を返す`hero`には、`Character`がもつフィールドしか要求できません。では、R2-D2のおもな機能(`primaryFunction`)を問い合わせたい場合はどうしたらよいでしょう？つぎのコードは、無効なクエリの例です。

```graphql:GraphQL
# INVALID: primaryFunction does not exist on Character
{
	hero {
		name
		primaryFunction
	}
}
```

```json:結果
{
	"errors": [
		{
			"message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline fragment on \"Droid\"?",
			"locations": [
				{
					"line": 5,
					"column": 5
				}
			]
		}
	]
}
```

`primaryFunction`は`Character`のフィールドではありません。そのため、このクエリは無効になるのです。`Character`が`Droid`のとき`primaryFunction`を取り出し、そうでなければこのフィールドは省くという手法が求められます。このとき用いるのが、前述のフラグメントです。フラグメントを`Droid`に定めて加えます。そうすることにより、`primaryFunction`が定められている型にのみクエリを実行できるのです。

```graphql:GraphQL
{
	hero {
		name
		...DroidFields
	}
}

fragment DroidFields on Droid {
	primaryFunction
}
```

```json:結果
{
	"data": {
		"hero": {
			"name": "R2-D2",
			"primaryFunction": "Astromech"
		}
	}
}
```

このクエリは有効とはいえ、少し冗長です。名前つきフラグメントは、何度も使い回すとき意味があります。けれど、このクエリを用いるのは1度きりです。そういうときは、インラインフラグメントが使えます。問い合わせる型は明らかに示しつつ、フラグメントに名前はつけません。

```graphql:GraphQL
{
	hero {
		name
		... on Droid {
			primaryFunction
		}
	}
}
```

```json:結果
{
	"data": {
		"hero": {
			"name": "R2-D2",
			"primaryFunction": "Astromech"
		}
	}
}
```

なお、GraphQLの仕様にもとづいた検証のコードは[`graphql-js/src/validation/`](https://github.com/graphql/graphql-js/tree/main/src/validation)に実装されています。

### シリーズGraphQLの基本
「[GraphQL: クエリ(queries)と変更(mutations)](https://qiita.com/FumioNonaka/items/f22bfc2928d56469bf93)」
「[GraphQL: スキーマと型](https://qiita.com/FumioNonaka/items/ff32ce5182ba328efbe7)」
「GraphQL: 検証(validation)」
「[GraphQL: 実行](https://qiita.com/FumioNonaka/items/611fe2d9698f36f268db)」
「[GraphQL: イントロスペクション(introspection)](https://qiita.com/FumioNonaka/items/5907b4f104a31c9df450)」