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

如何使用Kotlinx Serialization将带编号的JSON字段映射为列表

如何使用Kotlinx Serialization将带编号的JSON字段映射为列表

这种带编号的JSON字段确实挺让人头疼的,硬写1到20的循环总感觉不够优雅,我之前处理类似的API时也琢磨过,用kotlinx.serialization确实有更地道的解决方案,下面给你分享几种思路:

方法一:给Ingredients字段写自定义序列化器

这种方式只针对ingredients字段做自定义解析,其他字段依然用kotlinx.serialization的自动序列化,既简洁又符合框架的设计思路。

首先定义我们想要的数据结构:

@Serializable
data class Meal(
    val strMeal: String,
    val strCategory: String,
    // 其他你需要的字段...
    @Serializable(with = IngredientsListSerializer::class)
    val ingredients: List<Ingredient>
)

@Serializable
data class Ingredient(
    val name: String,
    val measure: String
)

然后实现自定义的序列化器IngredientsListSerializer,负责从带编号的字段中提取并组装成列表:

object IngredientsListSerializer : KSerializer<List<Ingredient>> {
    override val descriptor: SerialDescriptor = ListSerializer(Ingredient.serializer()).descriptor

    override fun deserialize(decoder: Decoder): List<Ingredient> {
        val jsonObject = decoder.decodeJsonElement() as JsonObject
        
        // 提取所有Ingredient和Measure字段,按编号分组
        val ingredientMap = jsonObject.entries
            .filter { it.key.startsWith("strIngredient") }
            .associate { entry ->
                val index = entry.key.substringAfter("strIngredient").toInt()
                index to entry.value.jsonPrimitive.contentOrNull
            }
            .filterValues { it != null } // 过滤掉空的食材

        val measureMap = jsonObject.entries
            .filter { it.key.startsWith("strMeasure") }
            .associate { entry ->
                val index = entry.key.substringAfter("strMeasure").toInt()
                index to entry.value.jsonPrimitive.contentOrNull ?: ""
            }

        // 按编号排序,组装成Ingredient列表
        return ingredientMap.keys.sorted().mapNotNull { index ->
            val name = ingredientMap[index] ?: return@mapNotNull null
            Ingredient(name, measureMap[index] ?: "")
        }
    }

    override fun serialize(encoder: Encoder, value: List<Ingredient>) {
        // 如果需要把列表序列化回带编号的字段格式,可以在这里实现
        val jsonObject = buildJsonObject {
            value.forEachIndexed { idx, ingredient ->
                put("strIngredient${idx + 1}", ingredient.name)
                put("strMeasure${idx + 1}", ingredient.measure)
            }
            // 可选:补全到API要求的20个空字段
            (value.size + 1..20).forEach { idx ->
                put("strIngredient$idx", null)
                put("strMeasure$idx", null)
            }
        }
        encoder.encodeJsonElement(jsonObject)
    }
}

这个方案的好处是完全集成到kotlinx.serialization的流程里,不用手动解析整个JSON,而且不需要硬编码1到20的范围,自动处理所有有效字段。

方法二:用扩展函数快速解析

如果不想写自定义序列化器,也可以用扩展函数来快速实现解析,这种方式更轻量,适合快速迭代的场景:

fun JsonObject.toIngredientsList(): List<Ingredient> {
    val ingredients = mutableListOf<Ingredient>()
    var currentIndex = 1
    
    // 循环直到遇到空的食材字段为止
    while (true) {
        val ingredientKey = "strIngredient$currentIndex"
        val measureKey = "strMeasure$currentIndex"
        
        val ingredientName = this[ingredientKey]?.jsonPrimitive?.contentOrNull ?: break
        val measure = this[measureKey]?.jsonPrimitive?.contentOrNull ?: ""
        
        ingredients.add(Ingredient(ingredientName, measure))
        currentIndex++
    }
    
    return ingredients
}

使用的时候,你可以先把JSON解析成JsonObject,再调用这个扩展函数:

val jsonObject = Json.parseToJsonElement(jsonString) as JsonObject
val meal = Meal(
    strMeal = jsonObject["strMeal"]?.jsonPrimitive?.contentOrNull ?: "",
    strCategory = jsonObject["strCategory"]?.jsonPrimitive?.contentOrNull ?: "",
    ingredients = jsonObject.toIngredientsList()
)

这种方式虽然用了循环,但不会硬写20的上限,而是自动在遇到空字段时停止,比硬编码范围要灵活得多。

方法三:自定义整个Meal类的序列化器

如果你的Meal类有很多字段需要自定义处理,也可以给整个类写序列化器,这种方式控制力最强,但代码量也会多一些:

object MealSerializer : KSerializer<Meal> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Meal") {
        element("strMeal", PrimitiveSerialDescriptor("strMeal", PrimitiveKind.STRING))
        element("strCategory", PrimitiveSerialDescriptor("strCategory", PrimitiveKind.STRING))
        element("ingredients", ListSerializer(Ingredient.serializer()).descriptor)
        // 添加其他字段的描述...
    }

    override fun deserialize(decoder: Decoder): Meal {
        val jsonObject = decoder.decodeJsonElement() as JsonObject
        
        // 解析普通字段
        val strMeal = jsonObject["strMeal"]?.jsonPrimitive?.contentOrNull ?: ""
        val strCategory = jsonObject["strCategory"]?.jsonPrimitive?.contentOrNull ?: ""
        
        // 解析食材列表(逻辑和方法一类似)
        val ingredientMap = jsonObject.entries
            .filter { it.key.startsWith("strIngredient") }
            .associate { entry ->
                val index = entry.key.substringAfter("strIngredient").toInt()
                index to entry.value.jsonPrimitive.contentOrNull
            }
            .filterValues { it != null }

        val measureMap = jsonObject.entries
            .filter { it.key.startsWith("strMeasure") }
            .associate { entry ->
                val index = entry.key.substringAfter("strMeasure").toInt()
                index to entry.value.jsonPrimitive.contentOrNull ?: ""
            }

        val ingredients = ingredientMap.keys.sorted().mapNotNull { index ->
            val name = ingredientMap[index] ?: return@mapNotNull null
            Ingredient(name, measureMap[index] ?: "")
        }

        return Meal(strMeal, strCategory, ingredients)
    }

    override fun serialize(encoder: Encoder, value: Meal) {
        // 序列化逻辑和方法一类似,这里不再重复
        val jsonObject = buildJsonObject {
            put("strMeal", value.strMeal)
            put("strCategory", value.strCategory)
            value.ingredients.forEachIndexed { idx, ing ->
                put("strIngredient${idx + 1}", ing.name)
                put("strMeasure${idx + 1}", ing.measure)
            }
            (value.ingredients.size + 1..20).forEach { idx ->
                put("strIngredient$idx", null)
                put("strMeasure$idx", null)
            }
        }
        encoder.encodeJsonElement(jsonObject)
    }
}

使用时给Meal类加上@Serializable(with = MealSerializer::class)注解即可。

总结

  • 如果只是处理食材列表,方法一是最推荐的,既简洁又符合kotlinx.serialization的设计理念。
  • 如果需要快速实现,方法二的扩展函数是不错的选择。
  • 如果有大量字段需要自定义处理,再考虑方法三的全类序列化器。

这些方案都避免了硬编码1到20的范围,让代码更灵活、更易维护。

备注:内容来源于stack exchange,提问作者Vivek Modi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.13 18:33:04