Introduction
Gson serializes objects by recursively traversing their field references. When two objects reference each other (a circular reference), Gson enters an infinite recursion that eventually causes a StackOverflowError. This is common in entity models with bidirectional relationships (parent-child, user-role, order-item) where each side holds a reference to the other.
Symptoms
java.lang.StackOverflowErrorduring Gson serialization- Crash only for objects with bidirectional relationships
- Works for simple objects but fails for complex entity graphs
- Infinite recursion in Gson stack trace
- Memory exhaustion before stack overflow on deep graphs
Stack trace:
``
java.lang.StackOverflowError: stack size 8192KB
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:173)
... (repeated thousands of times)
Common Causes
- Bidirectional JPA/Room entity relationships
- Parent-child relationships where child references parent
- Graph data structures with cycles
- DTO that includes full entity instead of ID reference
- Gson instance shared across different object graphs
Step-by-Step Fix
- 1.**Use @Expose to control which fields are serialized":
- 2.```kotlin
- 3.import com.google.gson.annotations.Expose
- 4.import com.google.gson.GsonBuilder
data class User( val id: Long, @Expose val name: String, @Expose val email: String, @Expose val orders: List<Order>? = null // Include orders )
data class Order( val id: Long, @Expose val total: Double, // DO NOT expose user - breaks circular reference // @Expose val user: User <-- Remove this val userId: Long? = null // Use ID instead )
// Create Gson with expose-only mode val gson = GsonBuilder() .excludeFieldsWithoutExposeAnnotation() .create()
val json = gson.toJson(user) // {"id": 1, "name": "John", "email": "john@example.com", "orders": [...]} // Order objects only contain userId, not full User reference ```
- 1.**Use ExclusionStrategy for circular reference detection":
- 2.```kotlin
- 3.import com.google.gson.ExclusionStrategy
- 4.import com.google.gson.FieldAttributes
- 5.import com.google.gson.GsonBuilder
class CircularReferenceExclusionStrategy : ExclusionStrategy { private val visitedObjects = ThreadLocal<MutableSet<Any>>()
override fun shouldSkipClass(clazz: Class<*>?): Boolean = false
override fun shouldSkipField(f: FieldAttributes): Boolean { // Skip fields that would create circular references val fieldName = f.name return fieldName == "parent" || fieldName == "owner" || fieldName == "user" // Customize per your model } }
val gson = GsonBuilder() .setExclusionStrategies(CircularReferenceExclusionStrategy()) .create() ```
- 1.**Use DTOs instead of entities for serialization":
- 2.```kotlin
- 3.// Entity - may have circular references
- 4.@Entity
- 5.data class UserEntity(
- 6.@Id val id: Long,
- 7.val name: String,
- 8.@OneToMany(mappedBy = "user")
- 9.val orders: List<OrderEntity> = emptyList()
- 10.)
@Entity data class OrderEntity( @Id val id: Long, val total: Double, @ManyToOne val user: UserEntity // Circular reference! )
// DTO - no circular references data class UserDto( val id: Long, val name: String, val orderIds: List<Long> // IDs only, no full objects )
data class OrderDto( val id: Long, val total: Double, val userId: Long // ID only )
// Map entity to DTO before serialization fun UserEntity.toDto() = UserDto( id = id, name = name, orderIds = orders.map { it.id } ) ```
- 1.**Use transient for fields that should not be serialized":
- 2.```kotlin
- 3.import com.google.gson.annotations.SerializedName
data class Category( val id: Long, val name: String, @Transient val parent: Category? = null, // Gson skips transient fields val subcategories: List<Category> = emptyList() )
// Or use @SerializedName to rename and exclude data class TreeNode( val id: Long, val value: String, // @Expose(deserialize = false, serialize = false) val parent: TreeNode? = null, // Gson skips if excludeFieldsWithoutExposeAnnotation val children: List<TreeNode> = emptyList() ) ```
Prevention
- Use DTOs for API serialization, never serialize entities directly
- Use
@ExposewithexcludeFieldsWithoutExposeAnnotation()to control fields - Replace object references with ID references in DTOs
- Use
@Transientfor fields that should never be serialized - Add circular reference detection to serialization tests
- Consider using kotlinx.serialization instead of Gson for Kotlin projects