Skip to content

reduceGeneratedSchemaTypes omits interface implementing objects from SchemaMetadata, breaking _asInlineFragment() at runtimeΒ #3632

@jasdeepsaini

Description

@jasdeepsaini

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.swift is generated
  • ❌ No case "CloudinaryImage" in SchemaMetadata.objectType(forTypename:)
  • βœ… But InterfaceTemplate still lists "CloudinaryImage" in implementingObjects (from the unfiltered _implementingObjects)

With reduceGeneratedSchemaTypes: false (or omitted):

  • βœ… Objects/CloudinaryImage.graphql.g.swift is generated
  • βœ… case "CloudinaryImage" present in SchemaMetadata
  • βœ… _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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions