LoginSignup
1

posted at

updated at

GraphQL: 検証(validation)

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

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

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

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

GraphQL
{
	hero {
		...NameAndAppearances
		friends {
			...NameAndAppearances
			friends {
				...NameAndAppearances
			}
		}
	}
}

fragment NameAndAppearances on Character {
	name
	appearsIn
}
結果
{
	"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
{
	hero {
		...NameAndAppearancesAndFriends
	}
}

fragment NameAndAppearancesAndFriends on Character {
	name
	appearsIn
	friends {
		...NameAndAppearancesAndFriends
	}
}
結果
{
	"errors": [
		{
			"message": "Cannot spread fragment \"NameAndAppearancesAndFriends\" within itself.",
			"locations": [
				{
					"line": 11,
					"column": 5
				}
			]
		}
	]
}

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

Cannot spread fragment "NameAndAppearancesAndFriends" within itself.

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

GraphQL
# 無効: `favoriteSpaceship`は`Character`にない
{
	hero {
		favoriteSpaceship
	}
}
結果
{
	"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".

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

GraphQL
# INVALID: heroはスカラーではないのでフィールドが必要
{
	hero
}
結果
{
	"errors": [
		{
			"message": "Field \"hero\" of type \"Character\" must have a selection of subfields. Did you mean \"hero { ... }\"?",
			"locations": [
				{
					"line": 3,
					"column": 3
				}
			]
		}
	]
}

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

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

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

GraphQL
# 無効: nameはスカラーなのでフィールドは要求できない
{
	hero {
		name {
			firstCharacterOfName
		}
	}
}
結果
{
	"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
# INVALID: primaryFunction does not exist on Character
{
	hero {
		name
		primaryFunction
	}
}
結果
{
	"errors": [
		{
			"message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline fragment on \"Droid\"?",
			"locations": [
				{
					"line": 5,
					"column": 5
				}
			]
		}
	]
}

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

GraphQL
{
	hero {
		name
		...DroidFields
	}
}

fragment DroidFields on Droid {
	primaryFunction
}
結果
{
	"data": {
		"hero": {
			"name": "R2-D2",
			"primaryFunction": "Astromech"
		}
	}
}

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

GraphQL
{
	hero {
		name
		... on Droid {
			primaryFunction
		}
	}
}
結果
{
	"data": {
		"hero": {
			"name": "R2-D2",
			"primaryFunction": "Astromech"
		}
	}
}

なお、GraphQLの仕様にもとづいた検証のコードはgraphql-js/src/validation/に実装されています。

シリーズGraphQLの基本

GraphQL: クエリ(queries)と変更(mutations)
GraphQL: スキーマと型
「GraphQL: 検証(validation)」
GraphQL: 実行
GraphQL: イントロスペクション(introspection)

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1