Response based codegen
Apollo Kotlin takes your GraphQL operations, generates Kotlin models for them and instantiates them from your JSON responses allowing you to access your data in a type safe way.
There are effectively 3 different domains at play:
-
The GraphQL domain: operations
The Kotlin domain: models
The JSON domain: responses
By default, Apollo Kotlin generates models that match 1:1 with your GraphQL operations. Inline and named fragments generate synthetic fields, so you can access GraphQL fragments with Kotlin code like data.hero.onDroid.primaryFunction
. Fragments are classes that can be reused from different operations. This code generation engine (codegen) is named operationBased
because it matches the GraphQL operation.
The Json response may have a different shape than your GraphQL operation though. This is the case when using merged fields or fragments. If you want to access your Kotlin properties as they are in the JSON response, Apollo Kotlin provides a responseBased
codegen that match 1:1 with the JSON response. GraphQL fragments are represented as Kotlin interfaces, so you can access their fields with Kotlin code like (data.hero as Droid).primaryFunction
. Because they map to the JSON responses, the responseBased
models have the property of allowing JSON streaming and/or mapping to dynamic JS objects. But because GraphQL is a very expressive language, it's also easy to create a GraphQL query that generate a very large JSON response.
For this reason and other limitations, we recommend using operationBased
codegen by default.
This page first recaps how operationBased
codegen works before explaining responseBased
codegen. Finally, it lists the different limitations coming with responseBased
codegen, so you can make an informed decision should you use this codegen.
To use a particular codegen, configure codegenModels
in your Gradle scripts:
1apollo {
2 service("service") {
3 // ...
4 codegenModels.set("responseBased")
5 }
6}
The operationBased
codegen (default)
The operationBased
codegen generates models following the shape of the operation.
A model is generated for each composite field selection.
Fragments spreads and inline fragments are generated as their own classes.
Merged fields are stored multiple times, once each time they are queried.
For example, given this query:
1query HeroForEpisode($ep: Episode!) {
2 search {
3 hero(episode: $ep) {
4 name
5 ... on Droid {
6 name
7 primaryFunction
8 }
9 ...HumanFields
10 }
11 }
12}
13
14fragment HumanFields on Human {
15 height
16}
The codegen generates these classes:
1class Search(
2 val hero: Hero?
3)
4
5class Hero(
6 val name: String,
7 val onDroid: OnDroid?,
8 val humanFields: HumanFields?
9)
10
11class OnDroid(
12 val name: String,
13 val primaryFunction: String
14)
1class HumanFields(
2 val height: Double
3)
Notice how onDroid
and humanFields
are nullable in the Hero
class. This is because they will be present or not depending on the concrete type of the returned hero:
1val hero = data.search?.hero
2when {
3 hero.onDroid != null -> {
4 // Hero is a Droid
5 println(hero.onDroid.primaryFunction)
6 }
7 hero.humanFields != null -> {
8 // Hero is a Human
9 println(hero.humanFields.height)
10 }
11 else -> {
12 // Hero is something else
13 println(hero.name)
14 }
15}
The responseBased
codegen
The responseBased
codegen differs from the operationBased
codegen in the following ways:
Generated models have a 1:1 mapping with the JSON structure received in an operation's response.
Polymorphism is handled by generating interfaces. Possible shapes are then defined as different classes that implement the corresponding interfaces.
Fragments are also generated as interfaces.
Any merged fields appear once in generated models.
Let's look at examples using fragments to highlight some of these differences.
Inline fragments
Consider this query:
1query HeroForEpisode($ep: Episode!) {
2 hero(episode: $ep) {
3 name
4 ... on Droid {
5 primaryFunction
6 }
7 ... on Human {
8 height
9 }
10 }
11}
If we run the responseBased
codegen on this operation, it generates a Hero
interface with three implementing classes:
DroidHero
HumanHero
OtherHero
Because Hero
is an interface with different implementations, you can use a when
clause to handle each different case:
1when (hero) {
2 is DroidHero -> println(hero.primaryFunction)
3 is HumanHero -> println(hero.height)
4 else -> {
5 // Account for other Hero types (including unknown ones)
6 // Note: in this example `name` is common to all Hero types
7 println(hero.name)
8 }
9}
Accessors
As a convenience, the responseBased
codegen generates methods with the name pattern as<ShapeName>
(e.g., asDroid
or asHuman
) that enable you to avoid manual casting:
1val primaryFunction = hero1.asDroid().primaryFunction
2val height = hero2.asHuman().height
Named fragments
Consider this example:
1query HeroForEpisode($ep: Episode!) {
2 hero(episode: $ep) {
3 name
4 ...DroidFields
5 ...HumanFields
6 }
7}
8
9fragment DroidFields on Droid {
10 primaryFunction
11}
12
13fragment HumanFields on Human {
14 height
15}
The responseBased
codegen generates interfaces for the DroidFields
and HumanFields
fragments:
1interface DroidFields {
2 val primaryFunction: String
3}
4
5interface HumanFields {
6 val height: Double
7}
These interfaces are implemented by subclasses of the generated HeroForEpisodeQuery.Data.Hero
(and other models for any operations using
these fragments):
1interface Hero {
2 val name: String
3}
4
5data class DroidHero(
6 override val name: String,
7 override val primaryFunction: String
8) : Hero, DroidFields
9
10data class HumanHero(
11 override val name: String,
12 override val height: Double
13) : Hero, HumanFields
14
15data class OtherHero(
16 override val name: String
17) : Hero
This can be used like so:
1when (hero) {
2 is DroidFields -> println(hero.primaryFunction)
3 is HumanFields -> println(hero.height)
4}
Accessors
As a convenience, the responseBased
codegen generates methods with the name pattern <fragmentName>
(e.g., droidFields
for a fragment named DroidFields
). This enables you to chain calls together, like so:
1val primaryFunction = hero1.droidFields().primaryFunction
2val height = hero2.humanFields().height
Limitations of responseBased
codegen
Because GraphQL is a very expressive language, it's easy to create a GraphQL query that generate a very large JSON response. If you're using a lot of nested fragments, the generated code size can grow exponentially with the nesting level. We have seen relatively small GraphQL queries breaking the JVM limits like maximum method size.
When using fragments, data classes must be generated for each operation where the fragments are used. To avoid name clashes, the models are nested and this comes with two side effects:
The resulting
.class
file name can be very long, breaking the 256 default maximum file name on macOS.Similarly named interfaces might be nested (for fragments). While this is valid in Kotlin, Java does not allow this, and it will break kapt if you're using it.
@include
,@skip
and@defer
directives are not supported on fragments inresponseBased
codegen. Supporting them would require generating twice the models each time one of these directive would be used.