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