如何使用Kotlinx.serialization将带编号的JSON字段映射到一个列表
我正在使用一个API(TheMealDB),它以这种格式返回配料:
{
"strIngredient1": "Salmon",
"strIngredient2": "Soy Sauce",
"strIngredient3": "Sugar",
...
"strIngredient20": null,
"strMeasure1": "2 fillets",
"strMeasure2": "3 tbsp",
"strMeasure3": "1 tbsp",
...
}
因此,它并不是返回像:
ingredients: [ { name: "Salmon", measure: "2 fillets" } m
这样的内容,而是暴露了固定编号的字段(strIngredient1..20, strMeasure1..20)。
我常见的做法大致如下:
for (i in 1..20) {
val ingredient = json\["strIngredient$i"\]?.jsonPrimitive?.contentOrNull
val measure = json\["strMeasure$i"\]?.jsonPrimitive?.contentOrNull
}
但我在想,是否有一种更地道的解决方案,使用 kotlinx.serialization 或者Kotlin本身?
例如:
使用自定义序列化器,自动映射动态JSON键
避免 硬编码 的 1..20 范围
对于暴露出这种编号字段的API,是否有更简洁的模式来处理?
解决方案
这是一个设计不佳的API的经典示例。处理这类响应,我有3 种做法。
- 在
JSONObject上定义一个扩展函数。
fun JsonObject.extractNumberedPairs(keyPrefix: String, valuePrefix: String): List<Pair<String, String>> =
keys.filter { it.startsWith(keyPrefix) }
.mapNotNull { key ->
val index = key.removePrefix(keyPrefix)
val keyVal = this[key]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() }
val valVal = this["$valuePrefix$index"]?.jsonPrimitive?.contentOrNull?.takeIf { it.isNotBlank() }
if (keyVal != null) keyVal to (valVal ?: "") else null
}
- 自定义序列化类
@Serializable(with = MealSerializer::class)
data class Meal( val id: String,val name: String,val ingredients:List<Ingredient>)
data class Ingredient(val name: String, val measure: String)
object MealSerializer : KSerializer<Meal> {
override val descriptor = buildClassSerialDescriptor("Meal")
override fun deserialize(decoder: Decoder): Meal {
val json = decoder.beginStructure(descriptor).let {
(decoder as JsonDecoder).decodeJsonElement().jsonObject
}
val ingredients = json.extractNumberedPairs("strIngredient", "strMeasure")
.map { (name, measure) -> Ingredient(name, measure) }
return Meal(
id = json["idMeal"]!!.jsonPrimitive.content,
name = json["strMeal"]!!.jsonPrimitive.content,
ingredients = ingredients
)
}
override fun serialize(encoder: Encoder, value: Meal) = throw UnsupportedOperationException()
}
- 原始手动映射器 - 这既简单又有点繁琐
// 1. Raw DTO — just captures the wire format
@Serializable
data class MealDto(
@SerialName("idMeal") val id: String,
@SerialName("strMeal") val name: String,
@SerialName("strIngredient1") val ingredient1: String? = null,
...
...
@SerialName("strIngredient20") val ingredient20: String? = null,
@SerialName("strMeasure1") val measure1: String? = null,
....
...
@SerialName("strMeasure20") val measure20: String? = null,
)
fun MealDto.toMeal(): Meal {
val rawIngredients = listOf(ingredient1, ingredient2, ... ,ingredient20)
val rawMeasures = listOf( measure1, measure2, ... ,measure20
)
val ingredients = rawIngredients
.zip(rawMeasures)
.filter { (name, _) -> !name.isNullOrBlank() }
.map { (name, measure) -> Ingredient(name!!, measure.orEmpty()) }
return Meal(id, name, ingredients)
}
我更喜欢第一种做法,它和你做的很相似,只是没有使用for循环,因为它会限制我们;如果配料超过20种,就会出问题。
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。