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:
apollo {service("service") {// ...codegenModels.set("responseBased")}}
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:
query HeroForEpisode($ep: Episode!) {search {hero(episode: $ep) {name... on Droid {nameprimaryFunction}...HumanFields}}}fragment HumanFields on Human {height}
The codegen generates these classes:
class Search(val hero: Hero?)class Hero(val name: String,val onDroid: OnDroid?,val humanFields: HumanFields?)class OnDroid(val name: String,val primaryFunction: String)
class HumanFields(val height: Double)
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:
val hero = data.search?.herowhen {hero.onDroid != null -> {// Hero is a Droidprintln(hero.onDroid.primaryFunction)}hero.humanFields != null -> {// Hero is a Humanprintln(hero.humanFields.height)}else -> {// Hero is something elseprintln(hero.name)}}
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:
query HeroForEpisode($ep: Episode!) {hero(episode: $ep) {name... on Droid {primaryFunction}... on Human {height}}}
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:
when (hero) {is DroidHero -> println(hero.primaryFunction)is HumanHero -> println(hero.height)else -> {// Account for other Hero types (including unknown ones)// Note: in this example `name` is common to all Hero typesprintln(hero.name)}}
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:
val primaryFunction = hero1.asDroid().primaryFunctionval height = hero2.asHuman().height
Named fragments
Consider this example:
query HeroForEpisode($ep: Episode!) {hero(episode: $ep) {name...DroidFields...HumanFields}}fragment DroidFields on Droid {primaryFunction}fragment HumanFields on Human {height}
The responseBased
codegen generates interfaces for the DroidFields
and HumanFields
fragments:
interface DroidFields {val primaryFunction: String}interface HumanFields {val height: Double}
These interfaces are implemented by subclasses of the generated HeroForEpisodeQuery.Data.Hero
(and other models for any operations using
these fragments):
interface Hero {val name: String}data class DroidHero(override val name: String,override val primaryFunction: String) : Hero, DroidFieldsdata class HumanHero(override val name: String,override val height: Double) : Hero, HumanFieldsdata class OtherHero(override val name: String) : Hero
This can be used like so:
when (hero) {is DroidFields -> println(hero.primaryFunction)is HumanFields -> println(hero.height)}
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:
val primaryFunction = hero1.droidFields().primaryFunctionval 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.
- The resulting
@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.