
本文详解如何在 Moshi 中为含固定字段(如 attributes)和任意额外字段的 JSON 设计自定义适配器,将动态属性统一捕获到 Map 中,弥补 Moshi 缺乏 @JsonAnySetter 的限制。
本文详解如何在 moshi 中为含固定字段(如 `attributes`)和任意额外字段的 json 设计自定义适配器,将动态属性统一捕获到 `map
Moshi 本身不提供类似 Jackson 的 @JsonAnySetter 注解,但可通过编写手动 JSON 读写适配器(Custom Adapter) 精确控制反序列化逻辑,完美支持“部分结构化 + 部分动态”的混合 JSON 场景。核心思路是:在 @FromJson 方法中遍历 JSON 对象的每个键值对,显式识别已知字段(如 "attributes"),其余键值对则动态注入 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 )</string></record>
✅ 编写自定义 Record 适配器(关键!)
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 option index")
}
}
reader.endObject()
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.entries.forEach { (key, v) ->
writer.name(key)
dynamicAdapter.toJson(writer, v)
}
writer.endObject()
}
}</any></attributes></string></attributes>
✅ 注册并使用适配器
val moshi = Moshi.Builder()
.add(RecordAdapter) // 注册自定义适配器
.build()
val bodyAdapter = moshi.adapter(Body::class.java)
// 示例 JSON(含固定 attributes + 动态 Id/Name/...)
val json = """{
"records": [{
"attributes": {"type": "Account", "url": "/services/data/v58.0/sobjects/Account/001..."},
"Id": "001...",
"Name": "Acme Corp",
"Industry": "Technology",
"AnnualRevenue": 150000000
}]
}"""
val body = bodyAdapter.fromJson(json)
println(body.records.first().recordBody["Name"]) // 输出: Acme Corp
println(bodyAdapter.toJson(body)) // 正确还原原始结构
⚠️ 注意事项与最佳实践
-
readJsonValue()是关键:它能递归解析任意嵌套的 JSON 值(对象、数组、字符串、数字、布尔、null),确保Map<string any></string>中的值类型与原始 JSON 一致。 -
selectName()提升性能与健壮性:相比nextName()+ 字符串匹配,selectName()使用预编译的选项列表,避免拼写错误且更高效;返回-1表示未命中,即动态字段。 -
双向支持需显式传入适配器:
@ToJson方法签名中必须声明JsonAdapter<any></any>(Moshi 内置支持),否则无法序列化Any类型值。 -
空值与重复字段校验:示例中加入了
attributes重复检测和非空校验,生产环境建议增强异常信息以利调试。 -
泛型适配器扩展性:若需复用该模式于其他类,可将
Record抽象为泛型模板,或封装为DynamicFieldAdapter<t></t>工具类。
通过此方案,你既能享受 Moshi 的编译时类型安全与高性能,又能灵活应对 Salesforce、REST API 等常见场景中“固定元数据 + 动态业务字段”的混合 JSON 结构,无需妥协于反射或运行时解析。











