You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何处理CoinMarketCap API含动态唯一键的嵌套JSON解析

解决CoinMarketCap API动态键的Kotlin Data Class适配问题

问题背景

调用CoinMarketCap单币种API时,返回的JSON中data字段下的键是该币种的唯一ID(比如比特币是"1",以太坊是"2"),动态变化:

{"data": {"1": {"id": 1,"name": "Bitcoin","symbol": "BTC",...}},"status": {"timestamp": "2023-02-02T16:19:58.335Z",...}}

自动生成的Data Class硬编码了@SerializedName("1"),导致只能解析比特币,其他币种返回null/默认值:

data class Data(
    @SerializedName("1")
    val x1: X1
)

解决方案

方法1:用Map接收动态键值对(最简单)

将data字段定义为Map<String, CryptoCurrency>,直接适配动态键,不管ID是什么都能正确解析。

修改后的完整Data Class:

// 顶层响应类
data class ApiResponse(
    val data: Map<String, CryptoCurrency>,
    val status: Status
)

// 币种详情类(原X1,重命名更语义化)
data class CryptoCurrency(
    @SerializedName("id")
    val id: Int,
    @SerializedName("name")
    val name: String,
    @SerializedName("symbol")
    val symbol: String,
    @SerializedName("slug")
    val slug: String,
    @SerializedName("is_active")
    val isActive: Int,
    @SerializedName("is_fiat")
    val isFiat: Int,
    @SerializedName("circulating_supply")
    val circulatingSupply: Long, // 修正为Long,避免数值溢出
    @SerializedName("total_supply")
    val totalSupply: Long,
    @SerializedName("max_supply")
    val maxSupply: Long?, // 部分币种无max supply,设为可空
    @SerializedName("date_added")
    val dateAdded: String,
    @SerializedName("num_market_pairs")
    val numMarketPairs: Int,
    @SerializedName("cmc_rank")
    val cmcRank: Int,
    @SerializedName("last_updated")
    val lastUpdated: String,
    @SerializedName("tags")
    val tags: List<String>,
    @SerializedName("platform")
    val platform: Any?, // 设为可空
    @SerializedName("self_reported_circulating_supply")
    val selfReportedCirculatingSupply: Any?,
    @SerializedName("self_reported_market_cap")
    val selfReportedMarketCap: Any?,
    @SerializedName("quote")
    val quote: Quote
)

// 报价类
data class Quote(
    @SerializedName("USD")
    val usd: UsdQuote
)

// USD报价详情类
data class UsdQuote(
    @SerializedName("price")
    val price: Double,
    @SerializedName("volume_24h")
    val volume24h: Double,
    @SerializedName("volume_change_24h")
    val volumeChange24h: Double,
    @SerializedName("percent_change_1h")
    val percentChange1h: Double,
    @SerializedName("percent_change_24h")
    val percentChange24h: Double,
    @SerializedName("percent_change_7d")
    val percentChange7d: Double,
    @SerializedName("percent_change_30d")
    val percentChange30d: Double,
    @SerializedName("market_cap")
    val marketCap: Double,
    @SerializedName("market_cap_dominance")
    val marketCapDominance: Int,
    @SerializedName("fully_diluted_market_cap")
    val fullyDilutedMarketCap: Double,
    @SerializedName("last_updated")
    val lastUpdated: String
)

// 状态类
data class Status(
    @SerializedName("timestamp")
    val timestamp: String,
    @SerializedName("error_code")
    val errorCode: Int,
    @SerializedName("error_message")
    val errorMessage: String,
    @SerializedName("elapsed")
    val elapsed: Int,
    @SerializedName("credit_count")
    val creditCount: Int
)

使用时,直接取Map的第一个值即可(单币种请求时Map只有一个条目):

val crypto = apiResponse.data.values.first()

方法2:自定义JSON反序列化器(直接拿到单个币种对象)

如果不想用Map,可自定义反序列化器,跳过动态键直接提取币种数据。以下是Gson的实现示例:

  1. 定义简化的ApiResponse:
data class ApiResponse(
    val data: CryptoCurrency?,
    val status: Status?
)
  1. 编写自定义反序列化器:
class CryptoResponseDeserializer : JsonDeserializer<ApiResponse> {
    override fun deserialize(
        json: JsonElement?,
        typeOfT: Type?,
        context: JsonDeserializationContext?
    ): ApiResponse {
        val jsonObject = json?.asJsonObject ?: throw JsonParseException("Invalid response JSON")
        
        // 解析status字段
        val status = context?.deserialize(jsonObject.get("status"), Status::class.java)
        
        // 提取data下的第一个(也是唯一一个)币种对象
        val dataObject = jsonObject.getAsJsonObject("data")
        val cryptoEntry = dataObject.entrySet().firstOrNull()
        val cryptoCurrency = cryptoEntry?.value?.let {
            context.deserialize(it, CryptoCurrency::class.java)
        }
        
        return ApiResponse(cryptoCurrency, status)
    }
}
  1. 注册反序列化器到Gson:
val gson = GsonBuilder()
    .registerTypeAdapter(ApiResponse::class.java, CryptoResponseDeserializer())
    .create()

之后用这个Gson实例解析响应,就能直接拿到ApiResponse.data中的单个币种对象。

注意事项

  • 修正字段类型:比如circulating_supply这类大数值要改用Long,避免Int溢出;可空字段(如max_supply、platform)要设为可空类型。
  • 单币种请求时,API返回的data下只会有一个键值对,所以用values.first()或entrySet().first()是安全的。

内容的提问来源于stack exchange,提问作者orhanuckulac

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 13:25:51