MongoPersistentEntityIndexResolver reports a spurious Found cycle when a generic class (e.g. Selection) appears at multiple levels of a document hierarchy with different type arguments. No actual cycle exists in the document structure.
Example structure:
@Document
public class FruitShop {
public Selection<FruitBasket> fruitBaskets;
}
public class FruitBasket {
public Selection<String> fruit;
}
public class Selection<T> {
public T include;
}
On startup, the index resolver logs:
2026-06-15T17:38:50.119+01:00 INFO 93629 --- [ Test worker] m.c.i.MongoPersistentEntityIndexResolver : Found cycle for field 'include' in type 'Selection' for path 'include -> fruit -> include'
Selection<String>.include is List<String> — a terminal type with no further entity graph to traverse. There is no cycle.
Root cause:
CycleGuard.protect() builds a Path of PersistentProperty<?> elements and detects a cycle when Path.append(breadcrumb) finds the breadcrumb already present in the path:
This uses PersistentProperty.equals(), which is implemented by AbstractPersistentProperty.equals() which in turn delegates to Property. This equals method only takes into account the raw type. So the include fields of type Selection are considered equal regardless of their generics - in the example these are Selection<FruitBasket> and Selection<String>.
Impact:
- The false
CyclicPropertyReferenceException causes the resolver to stop traversing that branch. Any @Indexed, @CompoundIndex, @TextIndexed, or @WildcardIndexed annotations on fields below the falsely-detected cycle will be ignored.
- Additional noise in the logs (especially when you have something more complex than the repro)
Expected behaviour:
The resolver should recognise that Selection and Selection are distinct types, and should only report a cycle when the same resolved parameterized type reappears (indicating genuine recursion, e.g. a tree node with List children).
Suggested fix direction:
The cycle detection needs to incorporate the resolved TypeInformation<?> (which I think may already be available).
Reproduction:
https://github.com/jameskennard/spring-data-mongodb-cycle-found-generics
MongoPersistentEntityIndexResolverreports a spuriousFound cyclewhen a generic class (e.g. Selection) appears at multiple levels of a document hierarchy with different type arguments. No actual cycle exists in the document structure.Example structure:
On startup, the index resolver logs:
Selection<String>.includeisList<String>— a terminal type with no further entity graph to traverse. There is no cycle.Root cause:
CycleGuard.protect()builds aPathofPersistentProperty<?>elements and detects a cycle whenPath.append(breadcrumb)finds the breadcrumb already present in the path:This uses
PersistentProperty.equals(), which is implemented byAbstractPersistentProperty.equals()which in turn delegates toProperty. This equals method only takes into account the raw type. So theincludefields of typeSelectionare considered equal regardless of their generics - in the example these areSelection<FruitBasket>andSelection<String>.Impact:
CyclicPropertyReferenceExceptioncauses the resolver to stop traversing that branch. Any@Indexed,@CompoundIndex,@TextIndexed, or@WildcardIndexedannotations on fields below the falsely-detected cycle will be ignored.Expected behaviour:
The resolver should recognise that Selection and Selection are distinct types, and should only report a cycle when the same resolved parameterized type reappears (indicating genuine recursion, e.g. a tree node with List children).
Suggested fix direction:
The cycle detection needs to incorporate the resolved TypeInformation<?> (which I think may already be available).
Reproduction:
https://github.com/jameskennard/spring-data-mongodb-cycle-found-generics