Join us from October 8-10 in New York City to learn the latest tips, trends, and news about GraphQL federation and API platform engineering.Join us for GraphQL Summit 2024 in NYC
Docs
Start for Free

Response based codegen


takes your , 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: s
  • 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 generate synthetic , so you can access GraphQL fragments with Kotlin code like data.hero.onDroid.primaryFunction. 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:

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

HeroQuery.graphql
query HeroForEpisode($ep: Episode!) {
search {
hero(episode: $ep) {
name
... on Droid {
name
primaryFunction
}
...HumanFields
}
}
}
fragment HumanFields on Human {
height
}

The codegen generates these classes:

HeroQuery.kt
class Search(
val hero: Hero?
)
class Hero(
val name: String,
val onDroid: OnDroid?,
val humanFields: HumanFields?
)
class OnDroid(
val name: String,
val primaryFunction: String
)
HumanFields.kt
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?.hero
when {
hero.onDroid != null -> {
// Hero is a Droid
println(hero.onDroid.primaryFunction)
}
hero.humanFields != null -> {
// Hero is a Human
println(hero.humanFields.height)
}
else -> {
// Hero is something else
println(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:

HeroQuery.graphql
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 types
println(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().primaryFunction
val height = hero2.asHuman().height

Named fragments

Consider this example:

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

HeroForEpisodeQuery.kt
interface Hero {
val name: String
}
data class DroidHero(
override val name: String,
override val primaryFunction: String
) : Hero, DroidFields
data class HumanHero(
override val name: String,
override val height: Double
) : Hero, HumanFields
data 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 named DroidFields). This enables you to chain calls together, like so:

val primaryFunction = hero1.droidFields().primaryFunction
val 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 are not supported on fragments in responseBased codegen. Supporting them would require generating twice the models each time one of these would be used.
Previous
JS Interoperability
Rate articleRateEdit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company