-
Notifications
You must be signed in to change notification settings - Fork 13
Codegen
retrofit-graphql provides an optional Gradle plugin for build-time code generation of typed GraphQL operations. The plugin is applied as:
plugins {
id("co.anitrend.retrofit.graphql.codegen") version "{latest_version}"
}pluginManagement {
repositories {
maven(url = uri("https://jitpack.io"))
gradlePluginPortal()
mavenCentral()
google()
}
}The DSL supports both single-target and multi-target modes. Multi-target is recommended for projects that have multiple GraphQL schemas:
retrofitGraphQL {
common {
// Shared settings across all targets
generateOperationConstants.set(true)
generateDocuments.set(true)
generateHashes.set(true)
generateVariables.set(true)
generateResponses.set(true)
serializationBackend.set(SerializationBackend.KOTLINX)
}
target("github") {
packageName.set("co.anitrend.retrofit.graphql.sample.generated")
schema.set(file("src/main/graphql/schema.graphql"))
operations.from(fileTree("src/main/graphql") {
include("**/*.graphql")
exclude("**/bucket/**")
})
scalars {
map("DateTime", "kotlin.String")
map("GitObjectID", "kotlin.String")
map("URI", "kotlin.String")
map("Upload", "kotlin.String")
}
}
}Codegen-only consumers need only the converter and the plugin:
dependencies {
implementation("com.github.AniTrend.retrofit-graphql:runtime:{tag}")
// :annotations and :android-assets pulled transitively via :runtime
}For kotlinx.serialization-annotated types (serializationBackend = KOTLINX), add the kotlinx runtime and apply the serialization compiler plugin in your module:
plugins {
kotlin("plugin.serialization")
}
dependencies {
implementation("com.github.AniTrend.retrofit-graphql:runtime:{tag}")
implementation("com.github.AniTrend.retrofit-graphql:serialization-kotlinx:{tag}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0")
}kotlinx-serialization is only needed when the generated types (or your runtime codec) use it. The public :api contracts are backend-neutral and require no serializer dependency; serializationBackend = NONE needs no serializer at all beyond your chosen runtime codec.
The plugin generates code into build/generated/source/graphql/{targetName}/:
These outputs are generated by default. Each can be disabled via its corresponding flag in the common {} or target {} DSL blocks (generateOperationConstants, generateDocuments, generateHashes):
| Output | Type | Description |
|---|---|---|
GeneratedGraphQLRegistry |
GraphQLDocumentRegistry |
Build-time operation document registry (backend-independent) |
GraphQLOperations |
Constants object | Operation name constants |
GraphQLDocuments |
Constants object | Operation document string constants (backend-independent; __typename is injected for abstract operations) |
GraphQLHashes |
Constants object | SHA-256 hash constants for APQ |
| Output | Type | Description |
|---|---|---|
| Enum classes | Kotlin enums | GraphQL enum types from the schema |
| Variable classes | Data classes implementing GraphQLVariables
|
Typed variable classes per operation |
| Input object classes | Data classes | GraphQL input types with default values |
| Request helpers |
.request(...) factory methods |
Type-safe GraphQLOperationRequest<VarType> constructors |
| Output | Type | Description |
|---|---|---|
| Response data classes | Per-backend annotated data classes |
{OperationName}Data with nested types for every selected object field |
| Sealed interfaces | For interfaces/unions |
__typename-based polymorphism via @JsonClassDiscriminator (KOTLINX) or plain sealed structures (NONE) |
The serializationBackend property controls which annotations are emitted on generated types:
retrofitGraphQL {
common {
serializationBackend.set(SerializationBackend.KOTLINX) // SerializationBackend.NONE, .KOTLINX, or .GSON
}
}| Backend | Annotations | Response Models | R8 Safety |
|---|---|---|---|
NONE |
None (strict) | Plain data holders; plain sealed interfaces for abstract types | N/A (no annotations) |
KOTLINX |
@Serializable, @SerialName, @JsonClassDiscriminator
|
Supported (including interface/union paths) | Automatic (kotlinx artifact consumer rules) |
GSON |
@SerializedName |
Supported (concrete types only) | Consumer-owned keep rules may be needed |
serializationBackend = NONE is used exactly as configured. There is no auto-selection: setting generateResponses = true with NONE emits plain, unannotated response models (including plain sealed interfaces for abstract types) with no serializer imports, annotations, or adapters:
retrofitGraphQL {
common {
generateResponses.set(true) // stays NONE: plain models, no KOTLINX upgrade
serializationBackend.set(SerializationBackend.NONE)
}
}The generated documents, hashes, and registry are backend-independent: the executable documents still inject __typename for abstract operations, so the wire payload is identical to the KOTLINX/GSON runs. With NONE, abstract-type dispatch on decode is the responsibility of your runtime codec or your own logic, since no discriminator annotations are emitted.
GSON + generateResponses = true is only supported for operations whose response types are fully concrete (no interfaces or unions). Gson cannot deserialize polymorphic sealed interfaces needed for GraphQL union and interface response types. Operations with abstract response types will fail at build time with a clear error message. Use KOTLINX for those operations, or use GSON only with generateResponses = false.
Generated names follow a strict contract:
-
Property names: lowerCamelCase (e.g.
createdAt,marketplaceListings) -
Class names: PascalCase (e.g.
GetMarketPlaceAppsData,MarketplaceListingsEdgesNode) -
Enum constants: SCREAMING_SNAKE_CASE (e.g.
OPEN,CREATED_AT)
Kotlin hard keywords and visibility modifiers are escaped by appending Value:
| GraphQL Name | Kotlin Name | @SerialName |
|---|---|---|
private |
privateValue |
"private" |
object |
objectValue |
"object" |
when |
whenValue |
"when" |
is |
isValue |
"is" |
The original wire name is always preserved in @SerialName, so JSON field names match the GraphQL schema regardless of Kotlin renaming.
Generated response classes use their default fully qualified kotlinx descriptor
names. Properties keep GraphQL wire names through property-level @SerialName:
@Serializable
data class GetCurrentUserData(@SerialName("viewer") val viewer: Viewer?) {
@Serializable
data class Viewer(@SerialName("login") val login: String)
}The backend-neutral factory requires an explicit runtime codec:
// Koin
single<GraphQLDocumentRegistry> { GeneratedGraphQLRegistry }
single {
GraphQLConverterFactory.create(
codec = KotlinxGraphQLTransportCodec(Json { ignoreUnknownKeys = true }),
registry = get(),
)
}The deprecated legacy GraphConverter (:compat) remains available for the asset-based flow:
// Legacy (deprecated, :compat)
single<GraphQLDocumentRegistry> { GeneratedGraphQLRegistry }
single {
val json = KotlinxGraphQLJson(Json { ignoreUnknownKeys = true })
GraphConverter.create(
context = androidContext(),
json = json,
registry = get(),
)
}When you have operations from multiple schemas (e.g. GitHub API + Bucket uploads), compose registries:
internal class CompositeGraphQLRegistry(
vararg delegates: GraphQLDocumentRegistry,
) : GraphQLDocumentRegistry {
private val registries: List<GraphQLDocumentRegistry> = delegates.toList()
override fun document(operationName: String): String? =
registries.firstNotNullOfOrNull { it.document(operationName) }
override fun hash(operationName: String): String? =
registries.firstNotNullOfOrNull { it.hash(operationName) }
}Registered in Koin:
single<GraphQLDocumentRegistry> {
CompositeGraphQLRegistry(
GeneratedGraphQLRegistry, // codegen output for GitHub schema
BucketGraphQLRegistry, // manually maintained for upload schema
)
}- Home — project overview
-
Migration Guide — migrating from
:libraryto modular dependencies - Switching to Codegen — moving from asset-based to codegen workflow
- Getting Started — quick start with codegen
- GraphQL File Uploads — multipart upload with generated types
- Persisted Queries — APQ setup
-
Serialization Backends — the
serializationBackendcodegen setting and runtime codecs - Naming Contract — keyword escaping and descriptor behavior rules
- Generated Response DTOs — using generated response models