Introduction
Gson serializes objects by recursively traversing their object graph. When objects reference each other (circular reference), Gson enters an infinite recursion loop that eventually causes a StackOverflowError. This commonly occurs with bidirectional relationships like parent-child entities, order-item relationships, or graph data structures.
Symptoms
java.lang.StackOverflowErrorduringgson.toJson()- Crash occurs only for objects with relationships, not simple objects
- Stack trace shows repeated Gson serialization calls
- Works in debug but crashes in release due to different stack sizes
- Error:
stack size 8192KBin Android crash logs
Example error:
``
java.lang.StackOverflowError: stack size 8192KB
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.write(ReflectiveTypeAdapterFactory.java:245)
at com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:69)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.write(ReflectiveTypeAdapterFactory.java:172)
... (repeats hundreds of times)
at com.example.Order.toJson(Order.kt:15)
Common Causes
- Bidirectional JPA/Room entity relationships (parent-child, order-items)
- Object graph with cycles (A references B, B references A)
- Serializing ORM entities directly without DTO conversion
- Nested tree structures where children reference parents
- Serializing Android Context or Activity references
Step-by-Step Fix
- 1.Exclude the back-reference field:
- 2.```kotlin
- 3.data class Order(
- 4.val id: String,
- 5.val items: List<OrderItem>
- 6.)
data class OrderItem( val id: String, val product: String, @Expose(serialize = false, deserialize = false) val order: Order? = null // Back-reference: do not serialize )
// Use GsonBuilder with Expose strategy val gson = GsonBuilder() .excludeFieldsWithoutExposeAnnotation() .create()
val json = gson.toJson(order) // OrderItems will not include the order back-reference ```
- 1.Use ExclusionStrategy for all back-references:
- 2.```kotlin
- 3.val gson = GsonBuilder()
- 4..addSerializationExclusionStrategy(object : ExclusionStrategy {
- 5.override fun shouldSkipField(f: FieldAttributes): Boolean {
- 6.// Skip fields that would cause circular references
- 7.return f.name == "parent" || f.name == "order" || f.name == "backing"
- 8.}
override fun shouldSkipClass(clazz: Class<*>): Boolean = false }) .create() ```
- 1.Write a custom TypeAdapter for the entity:
- 2.```kotlin
- 3.class OrderItemAdapter : JsonSerializer<OrderItem> {
- 4.override fun serialize(
- 5.src: OrderItem,
- 6.typeOfSrc: Type,
- 7.context: JsonSerializationContext
- 8.): JsonElement {
- 9.return JsonObject().apply {
- 10.addProperty("id", src.id)
- 11.addProperty("product", src.product)
- 12.addProperty("quantity", src.quantity)
- 13.// Intentionally omit the order back-reference
- 14.}
- 15.}
- 16.}
val gson = GsonBuilder() .registerTypeAdapter(OrderItem::class.java, OrderItemAdapter()) .create() ```
- 1.Convert to DTOs before serialization:
- 2.```kotlin
- 3.// Domain entities with circular references
- 4.data class User(val id: String, val posts: List<Post>)
- 5.data class Post(val id: String, val title: String, val author: User)
// DTOs for serialization (no circular references) data class UserDto( val id: String, val postIds: List<String> )
data class PostDto( val id: String, val title: String, val authorId: String )
// Conversion functions fun User.toDto() = UserDto(id, posts.map { it.id }) fun Post.toDto() = PostDto(id, title, author.id)
// Serialize DTOs, not entities val json = gson.toJson(user.toDto()) ```
Prevention
- Never serialize domain entities directly; always use DTOs
- Use
@Exposeannotation to explicitly control serialization - Consider using
kotlinx.serializationor Moshi which handle cycles better - Add unit tests that serialize all entity types
- Use ProGuard/R8 rules to verify serialization does not break in release builds
- Document which fields are back-references in entity class comments