如何处理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的实现示例:
- 定义简化的ApiResponse:
data class ApiResponse( val data: CryptoCurrency?, val status: Status? )
- 编写自定义反序列化器:
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) } }
- 注册反序列化器到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
相关产品推荐
相关产品推荐

