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:
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:
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:
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.
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 |