Skip to content

SchemaTransformer.transformSchema produces schema with dangling reference to deleted type #4133

Closed

Description

@alf

Describe the bug
We've implemented a visitor that prunes our schema based on the presence of a directive.
This has worked well until quite recently, but has suddenly started failing with graphql.AssertException: Assert type Rom not found in schema.

I've analyzed the problem and reduced the schema and our visitor down to the minimal needed to reproduce the issue. Note that our actual implementation also handles other schema elements, but I don't think this is relevant for the issue at hand.

To Reproduce

Given the following schema, our visitor crashes with AssertException:

directive @remove on FIELD_DEFINITION

type Query {
  rental: Rental @remove
  customer: Customer
}

type Store {
  inventory: Inventory @remove
}

type Inventory {
  store: Store @remove
}

type Customer {
  rental: Rental
  payment: Payment @remove
}

type Payment {
  inventory: Inventory @remove
}

type Rental {
  id: ID
  customer: Customer @remove
}

Our visitor is defined as follows:

    class SchemaPruningVisitor extends GraphQLTypeVisitorStub {
        @Override
        public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node, TraverserContext<GraphQLSchemaElement> context) {
            if (node.hasAppliedDirective("remove")) {
                return deleteNode(context);
            }

            return CONTINUE;
        }

        @Override
        public TraversalControl visitGraphQLObjectType(GraphQLObjectType node, TraverserContext<GraphQLSchemaElement> context) {
            if (node.getFields().stream().allMatch(field -> field.hasAppliedDirective("remove"))) {
                return deleteNode(context);
            }

            return CONTINUE;
        }
    }

Interesting observation

Note that if we flip the order of the Query.rental and Query.customer fields then our visitor no longer crashes and produces the schema we expect:

type Query {
  customer: Customer
}

type Customer {
  rental: Rental
}

type Rental {
  id: ID
}

Activity

  1. added
    keep-openTells Stale Bot to keep PRs and issues open
    on Oct 15, 2025
  2. bbakerman commented on Oct 16, 2025

    @bbakerman
    Member

    I have created a repo PR and confirm the assertion. We will look into this further

  3. andimarek commented on Oct 17, 2025

    @andimarek
    Member

    @alf in which version did this actually work, meaning from which version to version did you notice it failing?

  4. alf commented on Oct 17, 2025

    @alf
    Author

    I believe it started failing because we suddenly ended up with a schema that has the problematic structure, not because we upgraded graphql-java.

  5. andimarek commented on Oct 17, 2025

    @andimarek
    Member

    @alf yes ... I just wanted to comment on this. Indeed the problem comes from your circular structure and order of your fields.

    For example: the visitor actually never visits Rental because when it hits Query.rental the field is deleted and the visitor doesn't continue traversing down a deleted element.

    And when the traverser reaches Rental again via Query.customer -> Customer. rental the Customer.rental field definition doesn't actually reference the real rental Object but uses a GraphQLTypeReference. A GraphQLTypeReference is an internal placeholder type to deal with circular references.

    This means the actual Rental is never visited and hence still references the original Customer which indirectly references Inventory. But then on other hand Inventory is actually deleted implicitly because Customer.payment is deleted.
    This causes the exception you see.

    We need to look into the best way to fix this.

  6. alf commented on Oct 17, 2025

    @alf
    Author

    That's a good explanation, thank you.

  7. andimarek commented on Oct 17, 2025

    @andimarek
    Member

    Hi @alf ... please have a look at this: https://github.com/graphql-java/graphql-java/pull/4142/files#r2441444727

    in order to make sure you will visit every Object you need to add all object types to the schema as additionalTypes. This ensures that in your example you will always visit Rental regardless of the order of the fields (or how you change them).

    The small caveat here is that you need to avoid adding the Query/Mutation/Subscription types as this will lead to another strange error.

  8. alf commented on Oct 18, 2025

    @alf
    Author

    Nice, thank you. Is this something that the SchemaParser should be doing? That’s what we’re using to build our TDR.

  9. andimarek commented on Oct 18, 2025

    @andimarek
    Member

    Is this something that the SchemaParser should be doing?

    Not sure we should do that as default as for very large schemas it increases the traversal time and is simply unnecessary if you are not deleting nodes like in your use case.

    But we probably add a helper util or so to enable "alwaysTraverseAllNodes" to not make people write this on their own and to document this case better.

    @alf would you be open to contribute this improvement?

  10. alf commented on Oct 19, 2025

    @alf
    Author

    Sounds perfect for our use case. I’ll take a shot at contributing this.

  11. andimarek commented on Jan 19, 2026

    @andimarek
    Member

    I finally found time to add a built in way to handle these edge cases better. See #4209

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    keep-openTells Stale Bot to keep PRs and issues open

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions