Custom scalar types in Apollo Kotlin
In addition to its built-in scalar types (Int
, String
, etc.), GraphQL supports defining custom scalars. For example, your schema might define a custom scalar for Long
, Date
, BigDecimal
, or GeoPoint
.
Define class mapping
To interact with custom scalars in your Apollo Kotlin app, you need to define a mapping in your build.gradle[.kts]
file. This tells Apollo Kotlin which class to use to represent each custom scalar from your schema.
apollo {service("service") {mapScalar("GeoPoint", "com.example.GeoPoint")// Shortcuts exist for standard types - equivalent to mapScalar("Long", "kotlin.Long")mapScalarToKotlinLong("Long")}}
If needed, you can also do this with a built-in scalar (such as ID
) to override its default type.
Define class adapters
Each class you use to represent a custom scalar also requires an adapter to convert it to and from the JSON format that's sent over the network.
Each adapter requires a fromJson
function. A toJson
function is also required if your app ever passes the custom scalar as a GraphQL argument.
Here's an adapter for a GeoPoint
custom scalar:
class GeoPoint(val latitude: Double, val longitude: Double)val geoPointAdapter = object : Adapter<GeoPoint> {override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): GeoPoint {var latitude: Double? = nullvar longitude: Double? = nullreader.beginObject()while(reader.hasNext()) {when (reader.nextName()) {"latitude" -> latitude = reader.nextDouble()"longitude" -> longitude = reader.nextDouble()}}reader.endObject()// fromJson can throw on unexpected data and the exception will be wrapped in a// ApolloParseExceptionreturn GeoPoint(latitude!!, longitude!!)}// If you do not expect your scalar to be used as input, you can leave this method as TODO()override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: GeoPoint) {writer.beginObject()writer.name("latitude").value(value.latitude)writer.name("longitude").value(value.longitude)writer.endObject()}}
If you prefer working with Map
s, Apollo Kotlin comes with AnyAdapter
, which supports adapting String
, Int
, Double
, Boolean
, List
, and Map
. You can use it in an intermediate step:
val geoPointAdapter = object : Adapter<GeoPoint> {override fun fromJson(reader: JsonReader, customScalarAdapters: CustomScalarAdapters): GeoPoint {val map = AnyAdapter.fromJson(reader) as Map<String, Double>return GeoPoint(map["latitude"] as Double, map["longitude"] as Double)}override fun toJson(writer: JsonWriter, customScalarAdapters: CustomScalarAdapters, value: GeoPoint) {val map = mapOf("latitude" to value.latitude,"longitude" to value.longitude)AnyAdapter.toJson(writer, map)}}
This solution is more concise but slightly less performant.
Register adapters
After you define your adapters, you need to register them. This can be done either in the build.gradle[.kts]
file, or at runtime.
In build.gradle[.kts]
A third argument can be passed to mapScalar
to specify the adapter to use:
apollo {service("service") {mapScalar("GeoPoint", "com.example.GeoPoint", "com.example.Adapters.geoPointAdapter")}}
The given expression is copied as-is in the generated code. Therefore, it's possible to pass any of the following:
- An instantiation expression, like
"com.example.GeoPointAdapter()"
- A singleton reference, like
"com.example.GeoPointAdapter"
- A function call, like
"com.example.Adapters.getGeoPointAdapter()"
Make sure you pass the full class name including the package, because imports aren't automatically generated.
At runtime
You can also register adapters on your ApolloClient
instance by calling ApolloClient.Builder.addCustomScalarAdapter
once for each adapter:
val apolloClient = ApolloClient.Builder().serverUrl("https://example.com/graphql").addCustomScalarAdapter(GeoPoint.type, geoPointAdapter).build()
This method takes a type-safe generated class from Types
, along with its corresponding adapter.
If you can't find Types
, build your project to trigger codegen.
Apollo-provided adapters
The following built-in adapters can be used with common custom scalar types:
Adapter | Description |
---|---|
com.apollographql.apollo3.api.FloatAdapter | Converts from/to kotlin.Float /java.lang.Float |
com.apollographql.apollo3.api.LongAdapter | Converts from/to kotlin.Long /java.lang.Long |
In addition, the com.apollographql.apollo3:apollo-adapters
artifact provides these adapters:
Adapter | Description |
---|---|
com.apollographql.apollo3.adapter.KotlinxInstantAdapter | For kotlinx.datetime.Instant ISO8601 dates |
com.apollographql.apollo3.adapter.JavaInstantAdapter | For java.time.Instant ISO8601 dates |
com.apollographql.apollo3.adapter.KotlinxLocalDateAdapter | For kotlinx.datetime.LocalDate ISO8601 dates |
com.apollographql.apollo3.adapter.JavaLocalDateAdapter | For java.time.LocalDate ISO8601 dates |
com.apollographql.apollo3.adapter.KotlinxLocalDateTimeAdapter | For kotlinx.datetime.LocalDateTime ISO8601 dates |
com.apollographql.apollo3.adapter.JavaLocalDateTimeAdapter | For java.time.LocalDateTime ISO8601 dates |
com.apollographql.apollo3.adapter.KotlinxLocalTimeAdapter | For kotlinx.datetime.LocalTime ISO8601 dates |
com.apollographql.apollo3.adapter.JavaLocalTimeAdapter | For java.time.LocalTime ISO8601 dates |
com.apollographql.apollo3.adapter.JavaOffsetDateTimeAdapter | For java.time.OffsetDateTime ISO8601 dates |
com.apollographql.apollo3.adapter.DateAdapter | For java.util.Date ISO8601 dates |
com.apollographql.apollo3.adapter.BigDecimalAdapter | For a Multiplatform com.apollographql.apollo3.adapter.BigDecimal class holding big decimal values |
ⓘ NOTE
Because some adapters use kotlinx.datetime
(which itself uses java.time
), you need to enable core library desugaring on Android API levels < 26
For example, to use DateAdapter
, configure your Gradle scripts like so:
dependencies {implementation("com.apollographql.apollo3:apollo-adapters:4.0.0-beta.7")}apollo {service("service") {mapScalar("Date", "java.util.Date", "com.apollographql.apollo3.adapter.DateAdapter")}}