GraphQLで独自のScalarTypeを作ってみる!

GraphQLはSchemaに型を記述します。その型には2種類あります。

  • Object Type ... 取得できるデータの種類とそのフィールドを定義する型
  • Scalar Type ... Object Type のフィールドが最終的に変換される、それ以上分割できない型

イメージとしては Scalar Type が集まって Object Type を形成する感じだと思います。

Scalar Type は事前に用意されているものもありますが、独自に定義する事もできるみたいなので試してみました。

独自の Scalar Type を宣言する

Schemaでの宣言の方法は簡単で scalar {型名} と書くだけです。作ったSchemaの一部はこんな感じ。DataEmailを定義してQuerAuthorで使っています。

scalar Date
scalar Email

type Query {
    authorByEmail(email: Email): Author
}

type Author {
    id: ID
    firstName: String
    lastName: String
    age: Int
    birthDay: Date
    email: Email
}

サーバー側で処理する

では Email という Scalar Type のサーバー側での処理を実装してみます。

Scalar Type ごとにGraphQLScalarTypeインスタンスが必要なので作ります。GraphQLScalarTypenewScalarメソッドが用意されていて、メソッドチェーンをつかって新しいインスタンスを作れるようになっています。そのメソッドチェーンにはcoercingメソッドがあるのですが、引数に具体的な Scalar Type の変換処理クラスを渡します。

こんな感じですね。

val emailType: GraphQLScalarType = GraphQLScalarType.newScalar().name("Email").coercing(emailCoercing).build()

Scalar Type の具体的な変換処理はCoercingインターフェイスを実装したクラスで行います。上記の例ではemailCoercingというインスタンスがそのクラスに該当します。インターフェイスが実装するメソッドは以下の3つ。

  • parseValue ... クライアントが送ってきた変数をJavaの型に変換
  • parseLiteral ... クライアントが送ってきたqueryの引数をJavaの型に変換
  • serialize ... レスポンスに含める値をシリアライズ

全体のコードはこんな感じ。

package com.graphqljava.tutorial.bookdetails

import graphql.language.StringValue
import graphql.schema.Coercing
import graphql.schema.CoercingParseLiteralException
import graphql.schema.CoercingParseValueException
import graphql.schema.GraphQLScalarType
import org.slf4j.LoggerFactory

data class EmailScalarType(val rawValue: String)

val emailCoercing = object : Coercing<EmailScalarType, String> {

    private val logger = LoggerFactory.getLogger("emailCoercing")

    // Client to Server で値を受け取るときに呼び出される
    // 変数を変換する
    override fun parseValue(input: Any): EmailScalarType {
        logger.info("parseValue {}", input.toString())

        when (input) {
            // 入力された値はダウンキャストして使用する
            is String -> return EmailScalarType(input)
            // Coercing用の例外クラスを用いることで、適切なエラーレスポンスになる
            else -> throw CoercingParseValueException("invalid email value")
        }
    }

    // Client to Server で値を受け取るときに呼び出される
    // queryの引数を変換する
    override fun parseLiteral(input: Any): EmailScalarType {
        logger.info("parseLiteral {}", input.toString())

        when (input) {
            // 入力された値はダウンキャストして使用する
            is StringValue -> return EmailScalarType(input.value)
            // Coercing用の例外クラスを用いることで、適切なエラーレスポンスになる
            else -> throw CoercingParseLiteralException("invalid email value")
        }
    }

    // Server to Client で値を渡すときに呼び出される
    override fun serialize(dataFetcherResult: Any): String {
        logger.info("serialize {}", dataFetcherResult.toString())

        return when (dataFetcherResult) {
            is String -> dataFetcherResult
            is EmailScalarType -> dataFetcherResult.rawValue
            else -> dataFetcherResult.toString()
        }
    }

}

val emailType: GraphQLScalarType = GraphQLScalarType.newScalar().name("Email").coercing(emailCoercing).build()

あとはemailTypeRuntimeWiringにセットします。

    // 一部のコードは省略
    private fun buildWiring(): RuntimeWiring {
        return RuntimeWiring.newRuntimeWiring()
                .scalar(emailType)
                .build()
    }

以上の実装で Email Scalar Type が使えるようになりました!

このように Scalar Type を独自に作ることができます。また、一般的に使われそうなものは以下のGitHubリポジトリにアップされています。

今回はこの中から日付を表す Date 型をつかってみました。

実際に動かしてみる

実際に通信してみます。DataFetcherの実装は割愛します。

リクエス

{
  authorByEmail(email: "sample@demo.co.jp") {
    id
    birthDay
  }
}

レスポンス

{
  "data": {
    "authorByEmail": {
      "id": "author-1",
      "birthDay": "1990-01-01"
    }
  }
}

独自に定義された Scalar Type を使って通信することができました。