Description Of Bug
Generated GraphQL document strings can contain unresolved nested fragment spreads because codegen only inlines fragment references that are directly referenced by an operation. Fragments referenced from inside another fragment are left as bare ...FragmentName spreads without appending the corresponding fragment <Name> on <Type> { ... } definitions.
This produces invalid request documents for GraphQL servers because named fragment spreads must refer to fragment definitions that exist within the same document.
Consumer Impact
This was found during migration of the AniTrend Android app from asset-based GraphQL lookup to codegen-first lookup using retrofit-graphql 0.12.0.
Consumer stack:
- Project: AniTrend Android app, package
com.mxt.anitrend
- Runtime:
co.anitrend.retrofit.graphql.converter.request.GraphRequestConverter
- Codegen plugin:
com.github.AniTrend.retrofit-graphql:co.anitrend.retrofit.graphql.codegen.gradle.plugin:0.12.0
- Runtime library:
com.github.AniTrend.retrofit-graphql:runtime:0.12.0
- GraphQL API target: AniList GraphQL API
Observed through Chucker: outgoing requests contain nested fragment spreads such as:
...MediaTitleFragment
...MediaImageFragment
...FuzzyDateFragment
...AiringScheduleFragment
...MediaListFragmentMini
but the request body does not include matching fragment definitions such as:
fragment MediaTitleFragment on MediaTitle {
english
romaji
native
userPreferred
}
The server therefore receives a document with undefined fragment spreads. Depending on server validation and execution behavior, this can produce validation errors or missing/empty response fields.
Concrete Reproduction Shape
Consumer query:
query MediaBrowse($id: Int, $page: Int, $perPage: Int, ...) {
Page(page: $page, perPage: $perPage) {
pageInfo {
... PageInfoFragment
}
media(id: $id, ...) {
... MediaCoreFragment
}
}
}
MediaCoreFragment contains nested spreads:
fragment MediaCoreFragment on Media {
id
title {
... MediaTitleFragment
}
coverImage {
... MediaImageFragment
}
startDate {
... FuzzyDateFragment
}
endDate {
... FuzzyDateFragment
}
nextAiringEpisode {
... AiringScheduleFragment
}
mediaListEntry {
... MediaListFragmentMini
}
}
Generated document output in GraphQLDocuments.kt currently inlines the first-level spread, for example ...MediaCoreFragment -> ... on Media { ... }, but leaves the nested spreads intact. The generated document file contains no fragment definitions, so those nested spreads are unresolved at runtime.
Current Codegen Root Cause
codegen-core has a FragmentResolver that claims to recursively resolve nested fragments, but the current implementation only includes fragment definitions whose names are directly referenced by the original operation document:
val referencedNames = collectReferencedNames(document)
val relevantDefinitions = fragmentDefinitions.filterKeys { it in referencedNames }
When the original operation directly references only MediaCoreFragment, relevantDefinitions includes MediaCoreFragment but excludes fragments referenced inside MediaCoreFragment, such as MediaTitleFragment, MediaImageFragment, and FuzzyDateFragment.
The existing nested-fragment test also encodes this limitation. It directly references both the parent and nested fragment from the operation and comments that this is necessary because relevantDefinitions requires direct references for each fragment to be eligible for inlining.
That test does not cover the real consumer shape where the operation references only the parent fragment and the parent fragment references nested fragments transitively.
Runtime Path
The runtime is likely not the correct fix location.
GraphRequestConverter.resolveQuery() resolves the operation name, calls registry.document(operationName), and serializes the returned document string as the query field. It faithfully sends whatever GraphQLDocuments.kt generated.
Therefore the fix belongs in codegen document composition, not runtime request conversion.
Expected Behaviour
For every generated operation document:
- No named fragment spread should remain unresolved.
- Every named spread should either be fully inlined or have a matching fragment definition appended to the final document string.
- Nested/transitive fragment spreads should be resolved recursively.
- Missing fragment definitions should fail codegen with a clear error naming the missing fragment and source operation.
- Fragment cycles should fail codegen with a clear error that includes the cycle path.
Recommended Fix Direction
Choose one document composition strategy and make it explicit.
Option A: Recursive inline fragments
Recursively inline every named fragment spread as an inline fragment until no named spreads remain.
Example target shape:
query MediaBrowse(...) {
Page(...) {
media(...) {
... on Media {
title {
... on MediaTitle {
english
romaji
native
userPreferred
}
}
}
}
}
}
Option B: Append transitive fragment definitions
Keep named spreads, but append every transitively referenced fragment definition to the generated operation document.
Example target shape:
query MediaBrowse(...) {
Page(...) {
media(...) {
...MediaCoreFragment
}
}
}
fragment MediaCoreFragment on Media {
title {
...MediaTitleFragment
}
}
fragment MediaTitleFragment on MediaTitle {
english
romaji
native
userPreferred
}
Option B is often easier to reason about, preserves author-authored GraphQL more closely, and avoids excessive duplication across deeply nested fragments. Option A is also acceptable if implemented correctly and tested thoroughly.
Required Tests
Add codegen-core tests for the real consumer shape:
Parent fragment references nested fragment, operation references only parent
Input:
query GetMedia {
Media(id: 1) {
...MediaCoreFragment
}
}
fragment MediaCoreFragment on Media {
title {
...MediaTitleFragment
}
}
fragment MediaTitleFragment on MediaTitle {
romaji
english
}
Expected:
- Generated document contains no unresolved
...MediaTitleFragment, if using inline strategy.
- Or generated document includes
fragment MediaTitleFragment on MediaTitle, if using append-definition strategy.
- The final document is valid according to GraphQL fragment-spread validation rules.
Deep transitive fragment chain
Use at least three levels:
Operation -> FragmentA -> FragmentB -> FragmentC
Expected: all levels are resolved.
Sibling nested fragments
One parent fragment references multiple nested fragments:
MediaCoreFragment -> MediaTitleFragment
MediaCoreFragment -> MediaImageFragment
MediaCoreFragment -> FuzzyDateFragment
Expected: all sibling fragments are resolved.
Shared nested fragment
Two parent fragments reference the same nested fragment.
Expected:
- no duplicate fragment definitions if using append-definition strategy;
- no unresolved spreads if using inline strategy.
Missing nested fragment
Input:
Operation -> FragmentA -> MissingFragment
Expected: codegen fails with an error naming MissingFragment and the operation being generated.
Fragment cycle
Input:
FragmentA -> FragmentB -> FragmentA
Expected: codegen fails with a cycle error that identifies the path.
Acceptance Criteria
- Generated
GraphQLDocuments.kt contains valid executable GraphQL documents for operations with nested fragments.
- No generated document contains a named fragment spread whose definition is absent from that same document.
- The AniTrend Android
MediaBrowse generated document resolves all nested fragments under MediaCoreFragment, including:
MediaTitleFragment
MediaImageFragment
FuzzyDateFragment
AiringScheduleFragment
MediaListFragmentMini
- Existing first-level fragment behavior remains intact.
- Missing nested fragments fail at codegen time, not runtime.
- Cyclic nested fragments fail at codegen time.
- Runtime
GraphRequestConverter does not need to perform fragment resolution.
Related Context
The previous asset-based annotation scanner handled this by appending fragment definitions into the in-memory document before dispatch. Codegen needs to preserve that behavior so generated registry documents are equivalent to the previously working asset path.
Description Of Bug
Generated GraphQL document strings can contain unresolved nested fragment spreads because codegen only inlines fragment references that are directly referenced by an operation. Fragments referenced from inside another fragment are left as bare
...FragmentNamespreads without appending the correspondingfragment <Name> on <Type> { ... }definitions.This produces invalid request documents for GraphQL servers because named fragment spreads must refer to fragment definitions that exist within the same document.
Consumer Impact
This was found during migration of the AniTrend Android app from asset-based GraphQL lookup to codegen-first lookup using
retrofit-graphql0.12.0.Consumer stack:
com.mxt.anitrendco.anitrend.retrofit.graphql.converter.request.GraphRequestConvertercom.github.AniTrend.retrofit-graphql:co.anitrend.retrofit.graphql.codegen.gradle.plugin:0.12.0com.github.AniTrend.retrofit-graphql:runtime:0.12.0Observed through Chucker: outgoing requests contain nested fragment spreads such as:
but the request body does not include matching fragment definitions such as:
The server therefore receives a document with undefined fragment spreads. Depending on server validation and execution behavior, this can produce validation errors or missing/empty response fields.
Concrete Reproduction Shape
Consumer query:
MediaCoreFragmentcontains nested spreads:Generated document output in
GraphQLDocuments.ktcurrently inlines the first-level spread, for example...MediaCoreFragment -> ... on Media { ... }, but leaves the nested spreads intact. The generated document file contains nofragmentdefinitions, so those nested spreads are unresolved at runtime.Current Codegen Root Cause
codegen-corehas aFragmentResolverthat claims to recursively resolve nested fragments, but the current implementation only includes fragment definitions whose names are directly referenced by the original operation document:When the original operation directly references only
MediaCoreFragment,relevantDefinitionsincludesMediaCoreFragmentbut excludes fragments referenced insideMediaCoreFragment, such asMediaTitleFragment,MediaImageFragment, andFuzzyDateFragment.The existing nested-fragment test also encodes this limitation. It directly references both the parent and nested fragment from the operation and comments that this is necessary because
relevantDefinitionsrequires direct references for each fragment to be eligible for inlining.That test does not cover the real consumer shape where the operation references only the parent fragment and the parent fragment references nested fragments transitively.
Runtime Path
The runtime is likely not the correct fix location.
GraphRequestConverter.resolveQuery()resolves the operation name, callsregistry.document(operationName), and serializes the returned document string as thequeryfield. It faithfully sends whateverGraphQLDocuments.ktgenerated.Therefore the fix belongs in codegen document composition, not runtime request conversion.
Expected Behaviour
For every generated operation document:
Recommended Fix Direction
Choose one document composition strategy and make it explicit.
Option A: Recursive inline fragments
Recursively inline every named fragment spread as an inline fragment until no named spreads remain.
Example target shape:
Option B: Append transitive fragment definitions
Keep named spreads, but append every transitively referenced fragment definition to the generated operation document.
Example target shape:
Option B is often easier to reason about, preserves author-authored GraphQL more closely, and avoids excessive duplication across deeply nested fragments. Option A is also acceptable if implemented correctly and tested thoroughly.
Required Tests
Add codegen-core tests for the real consumer shape:
Parent fragment references nested fragment, operation references only parent
Input:
Expected:
...MediaTitleFragment, if using inline strategy.fragment MediaTitleFragment on MediaTitle, if using append-definition strategy.Deep transitive fragment chain
Use at least three levels:
Expected: all levels are resolved.
Sibling nested fragments
One parent fragment references multiple nested fragments:
Expected: all sibling fragments are resolved.
Shared nested fragment
Two parent fragments reference the same nested fragment.
Expected:
Missing nested fragment
Input:
Expected: codegen fails with an error naming
MissingFragmentand the operation being generated.Fragment cycle
Input:
Expected: codegen fails with a cycle error that identifies the path.
Acceptance Criteria
GraphQLDocuments.ktcontains valid executable GraphQL documents for operations with nested fragments.MediaBrowsegenerated document resolves all nested fragments underMediaCoreFragment, including:MediaTitleFragmentMediaImageFragmentFuzzyDateFragmentAiringScheduleFragmentMediaListFragmentMiniGraphRequestConverterdoes not need to perform fragment resolution.Related Context
The previous asset-based annotation scanner handled this by appending fragment definitions into the in-memory document before dispatch. Codegen needs to preserve that behavior so generated registry documents are equivalent to the previously working asset path.