如何使用Kotlinx.serialization将带编号的JSON字段映射到一个列表

移动开发 2026-07-12

我正在使用一个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 种做法。

  1. 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
    }
  1. 自定义序列化类
@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. 原始手动映射器 - 这既简单又有点繁琐
// 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导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章