LoginSignup
2
1

More than 1 year has passed since last update.

GraphQL: 検証(validation)

Last updated at Posted at 2022-04-12

本稿は公式サイト「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)

2
1
0

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
  3. You can use dark theme
What you can do with signing up
2
1