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.StackOverflowError during gson.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 8192KB in 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. 1.Exclude the back-reference field:
  2. 2.```kotlin
  3. 3.data class Order(
  4. 4.val id: String,
  5. 5.val items: List<OrderItem>
  6. 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. 1.Use ExclusionStrategy for all back-references:
  2. 2.```kotlin
  3. 3.val gson = GsonBuilder()
  4. 4..addSerializationExclusionStrategy(object : ExclusionStrategy {
  5. 5.override fun shouldSkipField(f: FieldAttributes): Boolean {
  6. 6.// Skip fields that would cause circular references
  7. 7.return f.name == "parent" || f.name == "order" || f.name == "backing"
  8. 8.}

override fun shouldSkipClass(clazz: Class<*>): Boolean = false }) .create() ```

  1. 1.Write a custom TypeAdapter for the entity:
  2. 2.```kotlin
  3. 3.class OrderItemAdapter : JsonSerializer<OrderItem> {
  4. 4.override fun serialize(
  5. 5.src: OrderItem,
  6. 6.typeOfSrc: Type,
  7. 7.context: JsonSerializationContext
  8. 8.): JsonElement {
  9. 9.return JsonObject().apply {
  10. 10.addProperty("id", src.id)
  11. 11.addProperty("product", src.product)
  12. 12.addProperty("quantity", src.quantity)
  13. 13.// Intentionally omit the order back-reference
  14. 14.}
  15. 15.}
  16. 16.}

val gson = GsonBuilder() .registerTypeAdapter(OrderItem::class.java, OrderItemAdapter()) .create() ```

  1. 1.Convert to DTOs before serialization:
  2. 2.```kotlin
  3. 3.// Domain entities with circular references
  4. 4.data class User(val id: String, val posts: List<Post>)
  5. 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 @Expose annotation to explicitly control serialization
  • Consider using kotlinx.serialization or 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