-
Notifications
You must be signed in to change notification settings - Fork 748
Description
Summary
When reduceGeneratedSchemaTypes is set to true, codegen excludes concrete object types that implement a referenced interface from SchemaMetadata.objectType(forTypename:). This causes _asInlineFragment() to silently return nil at runtime for inline fragments on those interface types, because Apollo cannot resolve the concrete type's interface conformance without the SchemaMetadata entry.
The interface's implementingObjects array is still populated correctly (unfiltered), so the generated code looks correct β but the actual Object type file and its SchemaMetadata case are missing.
Union member types are not affected β they are always added unconditionally in the union branch of the same function.
There is a PR with a fix that works well with our large GraphQL schema. I don't have any other schemas to test with, so not sure the fix will work broadly.
Version
2.0.6
Steps to reproduce the behavior
Schema
interface Image_Interface {
url: String!
}
interface UrlTemplateBackedImage_Interface implements Image_Interface {
url: String!
urlTemplate: String!
}
type CloudinaryImage implements Image_Interface & UrlTemplateBackedImage_Interface {
url: String!
urlTemplate: String!
}
type SimpleImage implements Image_Interface {
url: String!
}
type Photo {
image: Image_Interface!
}
type Query {
photo: Photo!
}Operation
query GetPhoto {
photo {
image {
url
... on UrlTemplateBackedImage_Interface {
urlTemplate
}
}
}
}Codegen Configuration
ApolloCodegenConfiguration(
// ...
options: .init(
reduceGeneratedSchemaTypes: true
)
)Expected Behavior
When the server returns "__typename": "CloudinaryImage", the generated code should resolve the inline fragment:
let image = response.data?.photo.image
let templateImage = image?.asUrlTemplateBackedImage_Interface
print(templateImage?.urlTemplate) // Expected: "https://example.com/image.jpg"Actual Behavior
asUrlTemplateBackedImage_Interface returns nil because CloudinaryImage is not registered in SchemaMetadata.objectType(forTypename:).
What happens during codegen
With reduceGeneratedSchemaTypes: true:
- β No
Objects/CloudinaryImage.graphql.g.swiftis generated - β No
case "CloudinaryImage"inSchemaMetadata.objectType(forTypename:) - β
But
InterfaceTemplatestill lists"CloudinaryImage"inimplementingObjects(from the unfiltered_implementingObjects)
With reduceGeneratedSchemaTypes: false (or omitted):
- β
Objects/CloudinaryImage.graphql.g.swiftis generated - β
case "CloudinaryImage"present inSchemaMetadata - β
_asInlineFragment()works correctly
Root Cause
In apollo-ios-codegen at Sources/GraphQLCompiler/JavaScript/src/compiler/index.ts, the addReferencedType() function filters interface implementing objects when reduceSchemaTypes is true:
if (isInterfaceType(type)) {
const possibleTypes = schema.getPossibleTypes(type);
(type as any)._implementingObjects = possibleTypes; // Unfiltered
for (const objectType of possibleTypes) {
if (!reduceSchemaTypes || hasTypePolicyDirective(objectType)) { // <-- Bug
addReferencedType(getNamedType(objectType))
}
}
}Only implementing objects with @typePolicy are added to referencedTypes. But _asInlineFragment() requires ALL implementing objects to be registered in SchemaMetadata for runtime type dispatch. This is inconsistent with union member types, which are always added unconditionally in the union branch of the same function.
Logs
N/A
Anything else?
This was introduced in commit d689a45f6ddd527fc77d8f62a7f2f0ad66b8d2f7 (PR apollographql/apollo-ios-dev#601). The PR's intent β reducing generated files for large schemas β is valid, but the filter is too aggressive: it doesn't account for the fact that SchemaMetadata entries are required at runtime for _asInlineFragment() to resolve type conditions.
Possible Fix
SchemaMetadata entries are only required for _asInlineFragment(), which only triggers when an interface appears in a type condition (... on SomeInterface). If an interface is only referenced as a field return type with no inline fragment, its implementing objects don't need SchemaMetadata entries at runtime.
A more targeted fix would differentiate the two cases inside addReferencedType():
if (isInterfaceType(type)) {
const possibleTypes = schema.getPossibleTypes(type);
(type as any)._implementingObjects = possibleTypes;
for (const objectType of possibleTypes) {
// includeForTypeCondition = this interface appears in an inline fragment type condition
if (!reduceSchemaTypes || hasTypePolicyDirective(objectType) || includeForTypeCondition) {
addReferencedType(getNamedType(objectType))
}
}
}Where includeForTypeCondition is set to true when addReferencedType() is called because of a type condition (... on InterfaceName), and false when called because of a field return type. This would:
- Fix the bug: inline fragment type conditions always register all implementing objects
- Preserve the optimization: implementing objects of interfaces used only as field return types (no inline fragments) can still be reduced
Environment:
- apollo-ios: 2.0.6
- apollo-ios-codegen: 2.0.6
- Xcode: 16.3
- Swift: 6.2