再利用可能なJSONレスポンスラッパーにKotlinジェネリクスを使用する

JSON APIレスポンスのラッピングのためにジェネリックなKotlin data classを作成する方法を学びます。型パラメータ、reified型、ジェネリック構造のシリアライゼーションを解説します。

Advanced Patterns

詳細な説明

JSONのためのジェネリックKotlin Data Class

複数のAPIエンドポイントが同じエンベロープ構造(ステータス、データ、ページネーション)を返すがdataペイロードが異なる場合、Kotlinジェネリクスでラッパーを一度定義して再利用できます。

典型的なAPIエンベロープJSON

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

ジェネリックラッパー

@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
)

異なるペイロードでの使用

// ユーザーリストエンドポイント用
val userResponse: ApiResponse<UserListData> =
    Json.decodeFromString(jsonString)

// 注文詳細エンドポイント用
val orderResponse: ApiResponse<OrderData> =
    Json.decodeFromString(jsonString)

Reified型によるインラインパース

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

val users = parseResponse<UserListData>(jsonString)

エラーエンベロープ

@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
)

nullableなdataerrorにより、同じ型で成功と失敗の両方を処理します:

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

Sealed Resultの代替

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>()
}

このアプローチはKotlinの型システムを使用して、エラーレスポンスでdataにアクセスすることを不可能にし、より強力なコンパイル時保証を提供します。

ユースケース

AndroidやKotlin MultiplatformのAPIクライアントライブラリを構築する際、同じレスポンスエンベロープを共有する数十のエンドポイントに遭遇します。ジェネリックラッパー型はボイラープレートを排除し、すべてのエンドポイントで一貫したエラー処理を保証します。

試してみる — JSON to Kotlin

フルツールを開く