
本文详解如何在 Moshi 中为混合结构(如固定 attributes 字段 + 任意其他键值对)设计自定义适配器,实现将动态属性统一捕获到 Map 中,弥补 Moshi 缺乏 @JsonAnySetter 的限制。
本文详解如何在 moshi 中为混合结构(如固定 `attributes` 字段 + 任意其他键值对)设计自定义适配器,实现将动态属性统一捕获到 `map
在实际开发中,尤其是对接 Salesforce、REST APIs 或遗留系统时,常会遇到「半结构化」JSON:其中一部分字段(如 attributes)语义明确、结构固定;其余字段则高度动态、键名不可预知(如 Id、Name、自定义字段等)。Moshi 默认不支持类似 Jackson 的 @JsonAnySetter,但可通过手动编写类型适配器(Type Adapter) 精准控制反序列化逻辑,兼顾健壮性与灵活性。
以下是一个生产就绪的解决方案,核心在于利用 JsonReader.Options 高效识别已知字段,并将剩余字段统一读取为 Map:
✅ 定义数据模型(保持简洁)
@JsonClass(generateAdapter = true) data class Body( val records: List<record> ) @JsonClass(generateAdapter = true) data class Record( val attributes: Attributes, val recordBody: Map<string any> ) @JsonClass(generateAdapter = true) data class Attributes( val type: String, val url: String )</string></record>
⚠️ 注意:
recordBody类型为Map,意味着它能容纳任意嵌套 JSON(对象、数组、字符串、数字等),这依赖于 Moshi 的readJsonValue()方法——它是 Moshi 原生支持的通用 JSON 解析能力。
✅ 实现健壮的自定义适配器
object RecordAdapter {
private val options = JsonReader.Options.of("attributes")
@FromJson
fun fromJson(reader: JsonReader, attributesAdapter: JsonAdapter<attributes>): Record {
reader.beginObject()
var attributes: Attributes? = null
val recordBody = mutableMapOf<string any>()
while (reader.hasNext()) {
when (reader.selectName(options)) {
0 -> { // 匹配到 "attributes"
if (attributes != null) throw JsonDataException("Duplicate 'attributes' field")
attributes = attributesAdapter.fromJson(reader) ?: throw JsonDataException("'attributes' cannot be null")
}
-1 -> { // 未匹配任何已知字段 → 动态键
val key = reader.nextName()
val value = reader.readJsonValue() ?: throw JsonDataException("Unexpected null value for key '$key'")
recordBody[key] = value
}
else -> throw AssertionError("Unexpected selectName result")
}
}
reader.endObject()
attributes ?: throw JsonDataException("'attributes' field is required but missing")
return Record(attributes, recordBody)
}
@ToJson
fun toJson(
writer: JsonWriter,
value: Record,
attributesAdapter: JsonAdapter<attributes>,
dynamicAdapter: JsonAdapter<any>
) {
writer.beginObject()
writer.name("attributes")
attributesAdapter.toJson(writer, value.attributes)
value.recordBody.forEach { (key, v) ->
writer.name(key)
dynamicAdapter.toJson(writer, v)
}
writer.endObject()
}
}</any></attributes></string></attributes>
关键设计说明:
-
JsonReader.Options.of("attributes"):声明仅需显式识别"attributes"字段,其余全部视为动态键。 -
selectName()+nextName()组合:先尝试匹配已知字段;若返回-1,说明是未知键,再调用nextName()获取键名,readJsonValue()安全解析任意类型值。 -
空值防护:显式校验
attributes是否存在、readJsonValue()返回值是否为null,避免静默失败。 -
双向支持:
@ToJson方法确保序列化时也能正确还原原始 JSON 结构(固定字段前置 + 动态字段后置)。
✅ 注册并使用适配器
readJsonValue()











