Use Kotlin Generics for Reusable JSON Response Wrappers

Learn how to create generic Kotlin data classes for wrapping JSON API responses. Covers type parameters, reified types, and serialization of generic structures.

Advanced Patterns

Detailed Explanation

Generic Kotlin Data Classes for JSON

When multiple API endpoints return the same envelope structure (status, data, pagination) but with different data payloads, Kotlin generics let you define the wrapper once and reuse it.

Typical API Envelope JSON

{
  "status": "ok",
  "data": {
    "users": [
      { "id": 1, "name": "Alice" }
    ]
  },
  "pagination": {
    "page": 1,
    "totalPages": 10
  }
}

Generic Wrapper

@Serializable
data class ApiResponse<T>(
    val status: String,
    val data: T,
    val pagination: Pagination? = null
)

@Serializable
data class Pagination(
    val page: Int,
    val totalPages: Int
)

Usage with Different Payloads

// For user list endpoint
val userResponse: ApiResponse<UserListData> =
    Json.decodeFromString(jsonString)

// For order detail endpoint
val orderResponse: ApiResponse<OrderData> =
    Json.decodeFromString(jsonString)

Reified Type for Inline Parsing

inline fun <reified T> parseResponse(json: String): ApiResponse<T> =
    Json.decodeFromString(json)

val users = parseResponse<UserListData>(jsonString)

Error Envelope

@Serializable
data class ApiResponse<T>(
    val status: String,
    val data: T? = null,
    val error: ApiError? = null,
    val pagination: Pagination? = null
)

@Serializable
data class ApiError(
    val code: String,
    val message: String
)

With nullable data and error, the same type handles both success and failure:

val response = parseResponse<UserListData>(jsonString)
if (response.error != null) {
    handleError(response.error)
} else {
    displayUsers(response.data!!)
}

Sealed Result Alternative

sealed class ApiResult<out T> {
    data class Success<T>(val data: T, val pagination: Pagination?) : ApiResult<T>()
    data class Error(val code: String, val message: String) : ApiResult<Nothing>()
}

This approach uses Kotlin's type system to make it impossible to access data on an error response, providing stronger compile-time guarantees.

Use Case

When building API client libraries for Android or Kotlin Multiplatform, you encounter dozens of endpoints that share the same response envelope. A generic wrapper type eliminates boilerplate and ensures consistent error handling across all endpoints.

Try It — JSON to Kotlin

Open full tool