本稿は公式サイト「Validation」にもとづく、GraphQLの構文検証の考え方についての解説です。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。なお、GraphQL公式サイトのコード例は、インタラクティブな環境です。コードを書き替えて結果が確かめられますので、ぜひ試してみてください。
型システムを用いることにより、GraphQLクエリが有効かどうかあらかじめ判断できます。無効なクエリがつくられたとき、実行時のチェックに頼ることなく、サーバーとクライアントは効果的に開発者に知らせられるのです。
スターウォーズの例については、GitHubのgraphql-js/src/__tests__/starWarsValidation-test.ts
に、無効かどうかを試すさまざまなクエリが含まれています。これは、参照の実装を検証できているか確かめるテストファイルです。
まずは、複雑でも有効なクエリを採り上げましょう。ネストされたクエリで、これまでご紹介したコード例と大きくは変わりません。ただし、重複するフィールドは、フラグメントに分けました。
{
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つ入れ子でした。この入れ子をなくします。
{
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.
また、フィールドを問い合わせるときは、その型に存在するフィールドを要求しなければなりません。hero
はCharacter
を返すのですから、問い合わせられるのはCharacter
がもつフィールドです。けれど、型の中にfavoriteSpaceship
というフィールドはありません。したがって、つぎのクエリも無効です。
# 無効: `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".
今度の無効なコード例は、フィールドを問い合わせたときにスカラーか列挙型以外が返ってくる場合です。そのフィールドからどういうデータを得たいのか、必ず指定しなければなりません。hero
はCharacter
を返し、name
とappearsIn
のフィールドが含まれています(「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
)は、フィールドhero
はCharacter
型なので、サブフィールドを指定するよう求めます。
Field "hero" of type "Character" must have a selection of subfields. Did you mean "hero { ... }"?
逆に、スカラーのフィールドは、その中にフィールドを含みません。フィールドを要求すれば、クエリは無効です。
# 無効: 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
)を問い合わせたい場合はどうしたらよいでしょう?つぎのコードは、無効なクエリの例です。
# 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
}
]
}
]
}
primaryFunction
はCharacter
のフィールドではありません。そのため、このクエリは無効になるのです。Character
がDroid
のときprimaryFunction
を取り出し、そうでなければこのフィールドは省くという手法が求められます。このとき用いるのが、前述のフラグメントです。フラグメントをDroid
に定めて加えます。そうすることにより、primaryFunction
が定められている型にのみクエリを実行できるのです。
{
hero {
name
...DroidFields
}
}
fragment DroidFields on Droid {
primaryFunction
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
このクエリは有効とはいえ、少し冗長です。名前つきフラグメントは、何度も使い回すとき意味があります。けれど、このクエリを用いるのは1度きりです。そういうときは、インラインフラグメントが使えます。問い合わせる型は明らかに示しつつ、フラグメントに名前はつけません。
{
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)」