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:

Kotlin
build.gradle.kts
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:

GraphQL
HeroQuery.graphql
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:

Kotlin
HeroQuery.kt
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)
Kotlin
HumanFields.kt
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:

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

GraphQL
HeroQuery.graphql
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:

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

Kotlin
1val primaryFunction = hero1.asDroid().primaryFunction
2val height = hero2.asHuman().height

Named fragments

Consider this example:

GraphQL
HeroQuery.graphql
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:

Kotlin
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):

Kotlin
HeroForEpisodeQuery.kt
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:

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

Kotlin
1val primaryFunction = hero1.droidFields().primaryFunction
2val height = hero2.humanFields().height

Limitations of responseBased codegen

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

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

  3. @include, @skip and @defer directives are not supported on fragments in responseBased codegen. Supporting them would require generating twice the models each time one of these directive would be used.

Feedback

Edit on GitHub

Forums