Description
When some<T>() encounters a circular reference (e.g., a data class A has a field of type A?), it currently throws SomeCircularReferenceException unconditionally. This makes it impossible to generate instances of recursive data structures without custom factories, even when the circular field is nullable and could gracefully resolve to null.
The change: when a circular reference is detected by ResolverChain, if nullableStrategy is NullOnCircularReference and the type is nullable (isMarkedNullable), return null instead of throwing. Non-nullable circular references should still throw.
NullOnCircularReference becomes the default strategy, replacing Random().
Scope
In scope:
- Add
NullOnCircularReference as a new variant of the NullableStrategy sealed interface
- Modify
ResolverChain.resolve() to check the strategy + nullability before throwing on circular reference
- Change the default in
SomeConfig from Random() to NullOnCircularReference
- Add unit tests for:
- Circular nullable field returns
null under NullOnCircularReference
- Circular non-nullable field still throws
SomeCircularReferenceException
- No behavior change for strategies that aren't
NullOnCircularReference
Out of scope:
- Changes to the exception class
- Any per-field circular reference control
Acceptance criteria
Description
When
some<T>()encounters a circular reference (e.g., a data classAhas a field of typeA?), it currently throwsSomeCircularReferenceExceptionunconditionally. This makes it impossible to generate instances of recursive data structures without custom factories, even when the circular field is nullable and could gracefully resolve tonull.The change: when a circular reference is detected by
ResolverChain, ifnullableStrategyisNullOnCircularReferenceand the type is nullable (isMarkedNullable), returnnullinstead of throwing. Non-nullable circular references should still throw.NullOnCircularReferencebecomes the default strategy, replacingRandom().Scope
In scope:
NullOnCircularReferenceas a new variant of theNullableStrategysealed interfaceResolverChain.resolve()to check the strategy + nullability before throwing on circular referenceSomeConfigfromRandom()toNullOnCircularReferencenullunderNullOnCircularReferenceSomeCircularReferenceExceptionNullOnCircularReferenceOut of scope:
Acceptance criteria
GIVEN a data class with a self-referencing nullable field (e.g.,
data class Node(val next: Node?))WHEN
some<Node>()is called with the default configTHEN the nullable field resolves to
nulland no exception is thrownGIVEN a data class with a self-referencing non-nullable field (e.g.,
data class Node(val next: Node))WHEN
some<Node>()is called with any strategyTHEN
SomeCircularReferenceExceptionis thrownGIVEN any existing strategy (
AlwaysNull,NeverNull,Random)WHEN a circular reference is detected
THEN the current behavior (throwing
SomeCircularReferenceException) is preserved unchanged