本稿は公式サイト「Schemas and Types」にもとづく、GraphQLの型システムと、それにより要求できるデータがどう記述できるかについての解説です。GraphQLはどのようなバックエンドフレームワークやプログラミング言語でも使えるので、実装の詳細は避け、考え方に重きを置きます。ドキュメントの邦訳ではなく、日本語で説明し直しました。原文から省いた部分もあり、逆にわかりにくいところは補っています。なお、GraphQL公式サイトのコード例は、インタラクティブな環境です。コードを書き替えて結果が確かめられますので、ぜひ試してみてください。
型システム
GraphQLクエリ言語の基本は、オブジェクトのフィールドを選ぶことです。たとえば、つぎのクエリのコード例で考えてみましょう
{
hero {
name
appearsIn
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
- おおもとの特別な
rootオブジェクトから開始。 - その下の
heroフィールドを選択。 -
heroから返されたオブジェクトから、nameとappearsInを選択
GraphQLのクエリの形状(shape)は、ほぼそのまま求める結果です。サーバーについてさほど知らなくても、クエリが何を返すかは予測できます。けれど、何を要求できるのか、データの正確な記述があると便利です。どのようなフィールドが選べるのか。どういうオブジェクトが返されるのか。子オブジェクトから取り出せるフィールドは何か。そのために用いられるのがスキーマです。
GraphQLサービスでは型の組み合わせを定めることにより、どのようなデータが得られるのかがはっきりと記述されます。クエリの問い合わせがあると、スキーマにもとづいて検証と実行がなされるのです。
型言語
GraphQLサービスは、どのような言語でも書けます。GraphQLスキーマを、JavaScriptのような特定のプログラミング言語の構文にもとづいて解説するのは適切ではありません。そこで、独自の簡易な「GraphQLスキーマ言語」を定義しましょう。GraphQLスキーマについて説明がしやすく、言語にとらわれない、クエリに似た言語です。
オブジェクト型とフィールド
GraphQLスキーマのもっとも基本となる要素がオブジェクト型です。サービスから取得できるオブジェクトを表し、どのようなフィールドがあるのか示します。つぎのコードがGraphQLスキーマ言語の例です。
type Character {
name: String!
appearsIn: [Episode!]!
}
このコードの要素を、ひとつずつ確認しましょう。
-
Character- GraphQLのオブジェクト型で、いくつかのフィールドをもつ。スキーマの型の多くはオブジェクト型。 -
nameおよびappearsIn-Characterがもつフィールド。Character型を操作するGraphQLクエリは、このふたつのフィールドだけが扱える。 -
String!-nullではない文字列スカラー型。-
String- 文字列として解決される組み込みスカラー型のひとつ。単独のスカラーオブジェクトなので、クエリでフィールドの中の入れ子の選択はできない。 -
!-nullではない(non-nullable)フィールドを示す。GraphQLサービスでこのフィールドのクエリは必ず値が返される。
-
-
[Episode!]!-Episodeオブジェクトの配列で、nullではない。したがって、appearsInフィールドのクエリは必ず配列(0以上の要素を含む)を返す。Episode!もnullではないので、要素は必ずEpisodeオブジェクトでなければならない。
以上がGraphQL型言語の基礎です。
引数
GraphQLオブジェクト型のすべてのフィールドには、必要があれば引数が与えられます。たとえば、つぎのコード例のlengthフィールドに添えたunitが引数です。
type Starship {
id: ID!
name: String!
length(unit: LengthUnit = METER): Float
}
引数にはすべて名前をつけます。GraphQLの引数は、すべて特定の名前で渡されるのです。JavaScriptやPythonといった言語の関数が、順番にならんだリストで引数を受け取るのと異なります。このコード例のlengthフィールドがもつのは、unitという名前で定められたひとつの引数です。
引数は必須にも省略可能にもできます。デフォルト値を与えられるのは、引数がオプションの場合です。lengthフィールドに引数が渡されないときは、unitにデフォルト値METERが設定されます。
Query型とMutation型
スキーマ内の型のほとんどは通常のオブジェクト型です。けれど、スキーマには特別なふたつの方があります。
schema {
query: Query
mutation: Mutation
}
すべてのGraphQLサービスはQuery型を備えます。Mutation型は加えても、加えなくても構いません。ふたつの型は、通常のオブジェクト型と同じです。ただし、GraphQLクエリのエントリポイントを定めるのが特別といえます。つぎのコード例を見ましょう。
query {
hero {
name
}
droid(id: "2000") {
name
}
}
{
"data": {
"hero": {
"name": "R2-D2"
},
"droid": {
"name": "C-3PO"
}
}
}
このGraphQLサービスのQuery型には、heroとdroidのふたつのフィールドが備わっていなければなりません。
type Query {
hero(episode: Episode): Character
droid(id: ID!): Droid
}
Mutation型も同じように扱えます。Mutation型に定めたフィールドは、呼び出すクエリの変更フィールドのルートで使えるのです。
スキーマの「エントリポイント」になるという特別な位置づけ以外は、QueryもMutationも他のGraphQLオブジェクト型と変わりません。定められたフィールドは、まったく同じように操作できるのです。
スカラー型
GraphQLのオブジェクトは名前とフィールドをもち、さらに入れ子にもできます。けれど、最終的にフィールドは具体的なデータに解決されなければなりません。それがスカラー型です。クエリの末端となります。つぎのコード例のクエリでは、フィールドnameとappearsInがスカラー型に解決されるのです。
{
hero {
name
appearsIn
}
}
{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
スカラー型は、その中にフィールドをもちません。クエリのツリーの末端だからです。GraphQLは、デフォルトでつぎのスカラー型を備えています。
-
Int- 符号つき32ビット整数。 -
Float- 符号つき倍精度浮動小数点値。 -
String- UTF-8文字列。 -
Boolean-trueかfalseかの真偽値。 -
ID- 一意の識別子を表す。オブジェクトの再読み込みやキャッシュのキーなどに用いられる。文字列と同じようにシリアル化されるが、人が読めることを意図しない。
ほとんどのGraphQLサービスでは、独自のスカラー型も定められるよう実装されています。たとえば、Date型の定義です。
scalar Date
型がどのようにシリアル化、逆シリアル化、および検証されるかは実装次第です。たとえば、Date型をつねに整数のタイムスタンプにシリアル化するように定められます。クライアントは、日付フィールドがどういう形式になるか知ることができるのです。
列挙型
列挙型は、取れる値の組み合わせが限定される特別なスカラーです。つぎのようなことができます。
- この型の引数が、許可された値のいずれかであることを確かめる。
- 型システムをとおして、フィールドが決まった値の組み合わせのうちのひとつだと伝える。
つぎのコードは、GraphQLスキーマ言語で列挙型を定めた例です。
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
この場合、スキーマでEpisodeという型を用いたら、NEWHOPE、EMPIRE、JEDIのいずれかひとつだと前提できます。
さまざまな言語のGraphQLサービスの実装は、言語ごとにそれぞれの列挙型の扱い方があることに注意してください。列挙型を正規にサポートする言語は、実装に採り入れるかもしれません。列挙型をサポートしていないJavaScriptなどの言語では、内部的に整数の組みとして扱うことも考えられます。けれど、そうした細かいことはクライアントには知らされません。列挙型の値は名前の文字列だけで操作できるのです。
リストとNon-Null型修飾子
GraphQLで定められる型は、オブジェクト型とスカラー、および列挙型だけです。けれど、スキーマのほかの部分やクエリ変数の宣言で用いられる型には、型修飾子を加えて検証に影響が与えられます。つぎのコードがその例です。
type Character {
name: String!
appearsIn: [Episode]!
}
フィールドnameはString型で、型名のあとに感嘆符!でnullでない(Non-Null)と定めました。サーバーがこのフィールドに返す値にnullは許されません。得られた値がnullであった場合には、GraphQL実行エラーが発生します。こうしてクライアントは、問題が起こったことを知らされるのです。
Non-Null型修飾子!は、フィールドの引数を定めるときにも使えます。GraphQL文字列または変数の引数にnull値が渡されると、GraphQLサーバーは検証エラーを返すのです。
query DroidById($id: ID!) {
droid(id: $id) {
name
}
}
{
"id": null
}
{
"errors": [
{
"message": "Variable \"$id\" of non-null type \"ID!\" must not be null.",
"locations": [
{
"line": 1,
"column": 17
}
]
}
]
}
リストも同じように型修飾子を用いて示します。リスト修飾子を用いたフィールドからは、その型の配列が返されるのです。スキーマ言語でリストは、型をを角括弧[]で囲んで表します。引数に用いることもでき、検証の段階で値は配列でなければなりません。
Non-Null修飾子とリスト修飾子は組み合わせても構いません。たとえば、つぎのコードはNon-Nullの文字列のリストを定めた例です。
myField: [String!]
リストそのものはnullでも構いません。空のリストも許されます。けれど、nullのメンバーはもてないのです。つぎのコードは、JSONの例です。
myField: null // 有効
myField: [] // 有効
myField: ['a', 'b'] // 有効
myField: ['a', null, 'b'] // エラー
Non-Null修飾子やリスト修飾子は、必要に応じていくつでも入れ子にできます。
インタフェース
多くの型システムと同じく、GraphQLはインタフェースをサポートします。インタフェースは抽象型で、実装するには定められた特定のフィールドの組みを型が備えなければなりません。
たとえば、つぎのコードはスターウォーズ3部作のキャラクターを表すインタフェースCharacterの例です。
interface Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
}
Characterを実装する型は、これらすべてのフィールドを正確に、それぞれの引数と戻り値の型で備えなければなりません。
たとえば、つぎのHumanとDroidがCharacterを実装できる型の例です。
type Human implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
starships: [Starship]
totalCredits: Int
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
appearsIn: [Episode]!
primaryFunction: String
}
ふたつの型はともに、Characterインタフェースが定めるすべてのフィールドを備えています。さらに、HumanがstarshipsとtotalCredits、DroidはprimaryFunctionという、実装したCharacterインタフェースにないそれぞれ独自の追加フィールドをもつのです。
インタフェースを用いると、オブジェクトやその組み合わせを返したいときに役立ちます。けれど、同じインタフェースを実装した異なる型があるかもしれません。たとえば、つぎのコードのクエリはエラーを起こすのです。
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
primaryFunction
}
}
{
"ep": "JEDI"
}
{
"errors": [
{
"message": "Cannot query field \"primaryFunction\" on type \"Character\". Did you mean to use an inline fragment on \"Droid\"?",
"locations": [
{
"line": 4,
"column": 5
}
]
}
]
}
heroフィールドはCharacter型を返します。そして、引数episodeに応じて、HumanかDroidのどちらかになるのです。このコード例のクエリでは、Characterインタフェースに備わるフィールドしか問い合わせられません。つまり、primaryFunctionは含まれないのです。
結果のエラー(errors)メッセージ(message)はつぎのように告げています。特定のオブジェクト型(Droid)のフィールドは、インラインフラグメントを用いて問い合わせなければならないのです。
Cannot query field "primaryFunction" on type "Character". Did you mean to use an inline fragment on "Droid"?"
query HeroForEpisode($ep: Episode!) {
hero(episode: $ep) {
name
... on Droid {
primaryFunction
}
}
}
{
"ep": "JEDI"
}
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
詳しくは、「GraphQL: クエリ(queries)と変更(mutations)」の「インラインフラグメント」をお読みください。
ユニオン型
ユニオン型はインターフェイスに似ている点もあります。けれど、型の間で共通のフィールドを指定することはできません。
union SearchResult = Human | Droid | Starship
スキーマの中でこのSearchResult型を返すと、得られるのはHuman、Droid、Starshipのいずれかです。ユニオン型のメンバーは具体的なオブジェクト型でなければならないことに注意してください。インタフェースや他のユニオンからユニオン型をつくることはできません。
ここで、SearchResultユニオン型を返すフィールドに問い合わせる場合、どのフィールドでも求められるようにするには、インラインフラグメントを用いることが必要です。
{
search(text: "an") {
__typename
... on Human {
name
height
}
... on Droid {
name
primaryFunction
}
... on Starship {
name
length
}
}
}
{
"data": {
"search": [
{
"__typename": "Human",
"name": "Han Solo",
"height": 1.8
},
{
"__typename": "Human",
"name": "Leia Organa",
"height": 1.5
},
{
"__typename": "Starship",
"name": "TIE Advanced x1",
"length": 9.2
}
]
}
}
__typenameフィールドはStringに解決され、クライアント上で異なるデータ型を互いに区別できます。
ところで、CharacterはHumanとDroidがともに実装するインタフェースでした。すると、共通のフィールドは複数の型からそれぞれ求めるのでなく、ひとつの場所にまとめて問い合わせられます(結果は、前掲コード例と変わりません)。
{
search(text: "an") {
__typename
... on Character {
name
}
... on Human {
height
}
... on Droid {
primaryFunction
}
... on Starship {
name
length
}
}
}
このコード例でインラインフラグメントのStarshipにはnameが指定されていることにご注意ください。StarshipはCharacterを実装しないため、nameは含めないと結果に表示されないのです。
入力型
これまで説明してきたのは、フィールドへの引数として列挙型や文字列などのスカラーを渡すことでした。けれど、複雑なオブジェクトでも簡単に渡せます。とくに役立つのは変更の場合で、つくったオブジェクトを丸ごと渡したいようなときです。GraphQLスキーマ言語では、入力型は通常のオブジェクトとまったく同じに見えます。ただし、typeの替わりに用いられるキーワードはinputです。
input ReviewInput {
stars: Int!
commentary: String
}
つぎのコードは、入力オブジェクト型を変更(mutation)で使いました。
mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
commentary
}
}
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
{
"data": {
"createReview": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}
入力オブジェクト型のフィールドは、さらに自身から入力オブジェクト型を参照できます。けれど、スキーマの中に入力型と出力型は混在できません。また、入力オブジェクト型のフィールドに、引数は与えられないのです。
シリーズGraphQLの基本
「GraphQL: クエリ(queries)と変更(mutations)」
「GraphQL: スキーマと型」
「GraphQL: 検証(validation)」
「GraphQL: 実行」
「GraphQL: イントロスペクション(introspection)」