
本文详解如何在 Moshi 中为混合结构(如固定 attributes 字段 + 任意其他键值对)设计自定义适配器,实现将动态属性统一捕获到 Map 中,弥补 Moshi 缺乏 @JsonAnySetter 的限制。
本文详解如何在 moshi 中为混合结构(如固定 `attributes` 字段 + 任意其他键值对)设计自定义适配器,实现将动态属性统一捕获到 `map
在实际开发中,尤其是对接 Salesforce、REST APIs 或遗留系统时,常会遇到「半结构化」JSON:其中一部分字段(如 attributes)语义明确、结构固定;其余字段则高度动态、键名不可预知(如 Id、Name、自定义字段等)。Moshi 默认不支持类似 Jackson 的 @JsonAnySetter,但可通过手动编写类型适配器(Type Adapter) 精准控制反序列化逻辑,兼顾健壮性与灵活性。
以下是一个生产就绪的解决方案,核心在于利用 JsonReader.Options 高效识别已知字段,并将剩余字段统一读取为 Map<string any></string>:
✅ 定义数据模型(保持简洁)
@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 )
⚠️ 注意:
recordBody类型为Map<string any></string>,意味着它能容纳任意嵌套 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()
}
}关键设计说明:
-
JsonReader.Options.of("attributes"):声明仅需显式识别"attributes"字段,其余全部视为动态键。 -
selectName()+nextName()组合:先尝试匹配已知字段;若返回-1,说明是未知键,再调用nextName()获取键名,readJsonValue()安全解析任意类型值。 -
空值防护:显式校验
attributes是否存在、readJsonValue()返回值是否为null,避免静默失败。 -
双向支持:
@ToJson方法确保序列化时也能正确还原原始 JSON 结构(固定字段前置 + 动态字段后置)。
✅ 注册并使用适配器
val moshi = Moshi.Builder()
.add(RecordAdapter) // 注册自定义适配器
.build()
val bodyAdapter = moshi.adapter<Body>(Body::class.java)
// 示例 JSON(含动态字段 Id/Name/...)
val json = """{
"records": [{
"attributes": {"type": "Account", "url": "/services/data/vXX.X/sobjects/Account/001..."},
"Id": "001...",
"Name": "Acme Corp",
"Industry": "Technology",
"CustomField__c": true
}]
}"""
val body = bodyAdapter.fromJson(json)!!
println(body.records.first().recordBody)
// 输出: {Id=001..., Name=Acme Corp, Industry=Technology, CustomField__c=true}? 注意事项与最佳实践
-
性能考量:
readJsonValue()会完整解析子树为 Kotlin 对象(如Map、List、String等),适用于中低频场景;若需极致性能且动态字段类型简单(如全为字符串),可改用nextString()等专用方法。 -
类型安全增强:若动态字段有部分已知模式,可在
recordBody后追加扩展函数或封装类(如Record.withDynamic<t>(key: String): T?</t>),提升调用侧安全性。 -
错误定位:在
fromJson中尽早抛出带上下文信息的异常(如字段名、位置),便于排查上游数据异常。 -
Moshi 版本兼容性:本方案适用于 Moshi 1.14+(
readJsonValue()为稳定 API);旧版本请升级或使用JsonReader.peek()+ 手动分支判断。
通过该方案,你无需妥协数据灵活性,也无需放弃 Moshi 的编译时类型安全与轻量优势——真正实现「结构化字段强校验 + 动态字段自由捕获」的完美平衡。


















