如何使用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
相关产品推荐
相关产品推荐

