💫

GraphQLの型をKotlinの型に変換する

2023/12/14に公開

初めに

ポート株式会社 サービス開発部 Advent Calendar 2023 14日目の記事です。

自分が携わっているサービスでは、APIにGraphQLを採用しています。
サーバーから、GraphQLによって取得・更新できるデータを、Androidで加工し、データを表示したり、更新したりしています。

その取得・更新するデータには、が存在します。StringやIntのようなスカラー型であったり、サービス独自のビジネスロジックが考慮された型であったり。。。

これらのGraphQLの型は、Android Kotlinの型(クラス)にそのまま対応しているものとそうでないものがあるので、対応していない場合は、どのようにKotlinの型に変換すればいいかを紹介していけたらと思います!

GraphQL

まず、簡単にGraphQLについて紹介します。

https://graphql.org/

GraphQLは、Facebookが開発したAPIのためのクエリ言語です。

特徴

GraphQLには以下の特徴があります。

  • クライアント側で取得・更新するデータを宣言できる
  • エンドポイントが1つしかない
  • 型とスキーマが存在する

クライアント側で取得・更新するデータを宣言できる

GraphQLは、クライアント側でデータの宣言をし、宣言されたデータをサーバーが返すようになっています。

例:ユーザーデータの内、ID・名前・有効かどうかの情報を取得する

{
  user {
    id
    name
    active
  }
}
{
  "data": {
    "user": {
      "id": 1,
      "name": "Test User",
      "active": true
    }
  }
}

エンドポイントが1つしかない

REST APIのように、複数のエンドポイントを設けず、単一のエンドポイントを使用します。
そのため、HTTPメソッドは取得も更新も全てPOSTになります。

例:ユーザーデータ取得をコールする

$ curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "{ user { id user active } }"}' \
https://example.com/graphql

型とスキーマが存在する

型については導入部分で軽く触れた通り、StringやIntのようなスカラー型が存在したり、独自の型を作成できたりします。

それらの型の情報は、スキーマに集約しています。

クライアントはこのスキーマを通じてデータを呼び出します。

schema.graphqls
type User {
  id: Int
  name: String
  active: Boolean
}

GraphQLのスキーマにも種類があるので、興味のある方は、以下の記事を読んでみてください。

https://zenn.dev/nekoshita/articles/7c454e8e552c0d

GraphQLの型

GraphQLの型の種類は以下のものが存在します。

  • スカラー型
  • オブジェクト型
  • 列挙型
  • 入力型
  • Query・Mutation
  • サブスクリプション型
  • リスト
  • null
  • インターフェース
  • ユニオン型

今回は、スカラー型に注目します。

スカラー型

https://graphql.org/learn/schema/#scalar-types

GraphQLがデフォルトで提供しているスカラー型は以下のものです。

  • Int: 符号がついた32bitの整数
  • Float: 符号がついた倍精度の浮動小数点値
  • String: UTF-8形式の文字列
  • Boolean: 真偽
  • ID: 一意の識別子。(値の実態はString)

基本的には、これらの型で表現していくのですが、サービス独自のスカラー型を定義することもできます。

例:日付を表すDate

"日付(UTC)"
scalar Date

Apollo Client For Kotlin

Apolloが開発したKotlinのGraphQLのクライアントライブラリー。
このライブラリーを使用することで、AndroidでもGraphQLを扱うことができます。

(今回は、導入する工程をスキップします。公式ドキュメントを参考に導入してみてください。)

https://www.apollographql.com/docs/kotlin

変換

GraphQLでサポートされてる型のKotlinへの変換は、ほとんどは、Apollo Client For Kotlinが自動で行ってくれます。

  • String → String
  • Int → Int
  • Float → Float
  • Boolean → Boolean
  • オブジェクト型, 入力型, Query・Mutation → data class
  • 列挙型 → enum class

etc...

変換ロジックを独自で組み立てる必要があるのは、サービス独自で作成したGraphQLの型です。

この型をKotlinの型に変換するには、Apollo Client For KotlinライブラリーのCustom scalarというものを使用します。

https://www.apollographql.com/docs/kotlin/essentials/custom-scalars

今回は、以下の型を変換していきます。

"ISO8601形式の日付"
scalar ISO8601Date

"ISO8601形式の日時"
scalar ISO8601DateTime

"符号がついた64bitの整数"
scalar BigInt

mapping

Apollo Clientを入れ、スキーマを適切な箇所に置き、Gradleで実行すると、スキーマから読み込んだ型をKotlinのクラスに変更してくれます。

Apollo Clientがスキーマの情報を取得し、Kotlinのクラスに変換するとき、Kotlinのどのクラスを使用するかをbuild.gradle.ktsに記載します。

mapScalarメソッドを使用すると、GraphQLで定義されているスカラー型とKotlinの型・クラスを紐付けすることができます。

また、mapScalarToKotlinLongメソッドのような、Kotlinの特定の型・クラスに変換してくれるメソッドも用意されています。

(公式のKDocにメソッド一覧があるので、適切なメソッドを使用するようにしましょう。)

build.gradle.kts
apollo {
    service("convert_sample") {
        mapScalar("ISO8601Date", "java.time.LocalDate")
	mapScalar("ISO8601DateTime", "java.time.LocalDateTime")
	
	// BigInt → kotlin.Long
	mapScalarToKotlinLong("BigInt")
    }
}

Adapter

レスポンスの形式がJSONであるため、実際はJSONの型をKotlinの型・クラスに変換する必要があります。

JSONの型は、String・Number・Booleanの3種類なので、数値と真偽以外の値は基本的に文字列としてデータが渡されます。
この時の文字列データをKotlinに変換するためのAdapterクラスを定義する必要があります。

今回は、ISO8601DateISO8601DateTimeAdapterクラスを用意してあげる必要があります。(BigIntmapScalarToKotlinLongというLongに変換する専用のメソッドを使用しているため、独自で定義しなくても大丈夫です。)

独自でAdapterクラスを作成するのもいいですし、Apollo Clientにはよく変換されるクラスのAdapterクラスを提供してくれているので、そのクラスを使用するのもありです。

https://www.apollographql.com/docs/kotlin/essentials/custom-scalars/#apollo-provided-adapters

Adapterクラスを作成する場合は、Adapter<T>インターフェースを継承させ、fromJsonメソッドとtoJsonメソッドを定義する必要があります。

import com.apollographql.apollo3.api.Adapter
import com.apollographql.apollo3.api.CustomScalarAdapters
import com.apollographql.apollo3.api.json.JsonReader
import com.apollographql.apollo3.api.json.JsonWriter
import java.time.LocalDate

class ISO8601DateAdapter: Adapter<LocalDate> {

    // JsonからKotlinに変換する
    override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): LocalDate {
        return LocalDate.parse(reader.nextString(), DateTimeFormatter.ISO_DATE)
    }
    
    // KotlinからJsonに変換する
    override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: LocalDate) {
        writer.value(value.toString())
    }
}

Adapter 適用

Adapterクラスを用意できたら、build.gradle.ktsmapScalarメソッドに追加します。もしくは、ApolloClientを作成するときにaddCustomScalarAdapterに渡してあげます。

build.gradle.kts
apollo {
    service("convert_sample") {
        // 作成したAdapterの場合
	mapScalar("ISO8601Date", "java.time.LocalDate", "com.example.ISO8601DateAdapter")
	
	// Apolloが提供しているAdapterの場合
	mapScalar("ISO8601DateTime", "java.time.LocalDateTime", "com.apollographql.apollo3.adapter.JavaLocalDateTimeAdapter")
    }
}

or


ApolloClient.Builder()
            .serverUrl("https://example.com/graphql")
            .okHttpClient(okHttpClient)
            .addCustomScalarAdapter(
	        CustomScalarType(name = "ISO8601Date", className = "java.time.LocalDate"),
		com.example.ISO8601DateAdapter
            )
	    .addCustomScalarAdapter(
	        CustomScalarType(name = "ISO8601DateTime", className = "java.time.LocalDateTime"),
		com.apollographql.apollo3.adapter.JavaLocalDateTimeAdapter
	    )
            .build()

これで、GraphQLの型がKotlinの型に変換されるはずです!

終わりに

GraphQLの型をKotlinの型に変換するのに、そこまで複雑な設定はいらないので、個人的には簡単に実装できるものだと思います。

ただ、実質な変換は、GraphQLの変換ではなく、JSONの変換なので、JSONに対しての知識も必要であると学びました。
また、型の範囲(主に数値系)を把握しておかないと適切な型に変換できないということも今回の対応で気づくことができました。

GraphQLの型をKotlinの適切な型に変換させることで、考慮することが減るので、これから他の型を対応する際もなんの型に変換するのかを吟味する必要がありそうです!

Discussion