Apollo Kotlin Execution Help

Generating a schema

Apollo Kotlin Execution uses KSP to parse your Kotlin code and generate a matching GraphQL schema.

To identify a graph, the Kotlin code must contain exactly one @GraphQLQuery class and at most one optional @GraphQLMutation and @GraphQLSubscription classes:

// Mandatory top-level Query @GraphQLQuery class Query { fun userById(id: String): User { } // ... } // Optional top-level Mutation @GraphQLMutation class Mutation { } // Optional top-level Subscription @GraphQLSubscription class Subscription { }

From those root classes, the Apollo Kotlin Execution processor traverses the Kotlin class graph and builds the matching GraphQL schema:

  • Classes used in output positions are mapped to objects, unions and interfaces.

  • Classes used in input positions are mapped to input objects.

  • Scalars and Enum can happen in both input and output position.

Whenever possible, the order of fields in the generated GraphQL schema is the same as the declaration order in Kotlin.

Objects and fields

Apollo Kotlin Execution maps public Kotlin classes to GraphQL types and public functions and properties to GraphQL fields with the same name:

Kotlin

GraphQL

class User(val id: String) { fun email(): String }
type User { id: String! email: String! }

Private/internal fields and functions are not exposed in GraphQL:

Kotlin

GraphQL

class User { fun email(): String internal fun sendVerificationEmail() }
type User { email: String! }

Unions and interfaces

Non-empty Kotlin interfaces are mapped to GraphQL interfaces:

Kotlin

GraphQL

sealed interface Node { id: String } class User( override val id: String ) : Node
interface Node { id: String! } type User implements Node { id: String! }

Empty Kotlin interfaces are mapped to GraphQL unions:

Kotlin

GraphQL

sealed interface Actor class User( val id: String, val email: String ) : Actor class Organisation( val users: List<User> ) : Actor
union Actor = User | Organisation type User { val id: String! val email: String! } type Organisation { val users: [User] }

Enums

Kotlin enums are mapped to GraphQL enums:

Kotlin

GraphQL

enum class Role { Read, Write, Admin }
enum Role { Read, Write, Admin }

Scalars

Classes annotated with @GraphQLScalar are mapped to GraphQL scalars:

Kotlin

GraphQL

@GraphQLScalar(GeoPointCoercing::class) class GeoPoint( val latitude: Double, val longitude: Double )
scalar GeoPoint

You can also map existing classes that you don't own to GraphQL scalars using typealias

Kotlin

GraphQL

import kotlinx.datetime.LocalDateTime @GraphQLScalar(DateTimeCoercing::class) typealias DateTime = LocalDateTime
scalar DateTime

Arguments

Kotlin parameters are mapped to GraphQL arguments:

Kotlin

GraphQL

class Organisation { fun users( first: Int, after: String ): UserConnection }
type Organisation { users( first: Int!, after: String! ): UserConnection! }

Use Optional<> anf @GraphQLDefault to further control your input values.

Note that because nullability vs optionality are so closely related in GraphQL, not all combination are allowed:

type Query { // A non-null argument fun fieldA(arg: Int) = 0 // Disallowed: argument type is nullable and doesn't have a default value: it must also be optional. //fun fieldB(arg: Int?) = 0 // An nullable argument, the client may omit it in which case the function body must handle `Absent`. fun fieldC(arg: Optional<Int?>) = 0 // Disallowed: argument type is not nullable and cannot be optional //fun fieldD(arg: Optional<Int>) = 0 // An non-null argument, if the client omits it, the default value will be used. fun fieldE(@GraphQLDefault("10") arg: Int) = 0 // An nullable argument, if the client omits it, the default value will be used. fun fieldF(@GraphQLDefault("10") arg: Int?) = 0 // Disallowed: there is a default value and the argument type cannot be optional //fun fieldG(@GraphQLDefault("10") arg: Optional<Int?>) = 0 // Disallowed: there is a default value and the argument type cannot be optional //fun fieldH(@GraphQLDefault("10") arg: Optional<Int>) = 0 }

Kotlin parameters may be of class type in which case the class is generated as a GraphQL input object:

Kotlin

GraphQL

class UserFilter( val role: Role?, val organisationId: String? ) class Query { fun users( where: UserFilter, ): List<User> }
input UserFilter { role: Role, organisationId: String } type Query { users( where: UserFilter!, ): [User!] }

Names

The GraphQL name can be customized using @GraphQLName:

Kotlin

GraphQL

@GraphQLName("User") class DomainUser { @GraphQLName("email") fun emailAddress(): String }
type User { email: String }

Descriptions

The GraphQL description is generated from the KDoc:

Kotlin

GraphQL

/** * A logged-in user of the service. */ class User { /** * The email address of the user. */ fun email(): String }
""" A logged-in user of the service. """ type User { """ The email address of the user. """ email: String }

Nullability

Nullable fields and input fields are also nullable in GraphQL:

Kotlin

GraphQL

class User { fun email(): String? fun id(): String }
type User { email: String id: String! }

Directives

Define custom directives using @GraphQLDirective:

Kotlin

GraphQL

/** * A field requires a specific opt-in */ @GraphQLDirective annotation class requiresOptIn(val feature: String)
""" A field requires a specific opt-in """ directive @requiresOptIn(feature: String!) on FIELD_DEFINITION

Use your directive by annotating your Kotlin code:

Kotlin

GraphQL

class Query { @requiresOptIn(feature = "experimental") fun experimentalField(): String { TODO() } }
type Query { experimentalField: String! @requiresOptIn(feature: "experimental") }

The order of the directives in the GraphQL schema is the same as the order of the annotations in the Kotlin code.

Kotlin annotation classes are mapped to input objects when used as parameter types:

Kotlin

GraphQL

enum class OptInLevel { Ignore, Warning, Error } annotation class OptInFeature( val name: String, val level: OptInLevel ) @GraphQLDirective annotation class requiresOptIn(val feature: OptInFeature)
enum OptInLevel { Ignore, Warning, Error } input OptInFeature { name: String!, level: OptInLevel! } directive @requiresOptIn(feature: OptInFeature!) on FIELD_DEFINITION

Limitations:

  1. It is impossible to reuse other input objects in directive arguments. The directive input objects can only be used in directives. This is because Kotlin annotations do not support regular class paramaters, only annotation classes unlike regular function parameters.

  2. Directive arguments cannot have default values. This is because KSP does not support reading Kotlin default values and using @GraphQLDefault only without a default value would not compile.

Deprecation

Kotlin symbols annotated as @Deprecated are marked deprecated in GraphQL when applicable:

Kotlin

GraphQL

class User { fun role(): Role @Deprecated("Check for `role == Admin` instead") fun isAdmin(): Boolean }
type User { isAdmin: String @deprecated("Check for `role == Admin` instead") }

Default values

KSP cannot read default parameter values.

In order to define a default value for your arguments, use @GraphQLDefault and pass the value encoded as GraphQL:

Kotlin

GraphQL

class Organisation { fun users( @GraphQLDefault("100") first: Int, @GraphQLDefault("null") after: String? ): UserConnection }
type Organisation { users( first: Int! = 100, after: String = null ): UserConnection! }

Built-in types

Kotlin built-in types map to their GraphQL equivalent:

Kotlin

GraphQL

Int

Int

String

String

Double

Float

Boolean

Boolean

List

List

Last modified: 08 November 2024