
本文介绍在 Moshi 中优雅处理混合结构 JSON(如 Salesforce REST API 响应)的方法:将已知字段(如 attributes)映射到具体属性,其余动态字段统一捕获到 Map 中,无需依赖字段顺序或类型假设。
本文介绍在 moshi 中优雅处理混合结构 json(如 salesforce rest api 响应)的方法:将已知字段(如 `attributes`)映射到具体属性,其余动态字段统一捕获到 `map
Moshi 本身不提供类似 Jackson 的 @JsonAnySetter 注解,但其强大的自定义适配器(Custom Adapter)机制可完美实现该需求。核心思路是:跳过已知字段,将剩余所有键值对读取为动态 Map,同时保持类型安全与反序列化健壮性。
以下是一个生产就绪的解决方案,使用 JsonReader.Options 高效识别已知字段,并通过 reader.readJsonValue() 保留原始 JSON 类型(支持 String、Number、Boolean、List<any></any>、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.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(含动态字段 Id/Name/...)
val json = """
{
"records": [{
"attributes": { "type": "Account", "url": "/services/data/v58.0/sobjects/Account/001..." },
"Id": "001...",
"Name": "Acme Corp",
"Industry": "Technology",
"AnnualRevenue": 125000000.0
}]
}
""".trimIndent()
val body = bodyAdapter.fromJson(json)
println(body.records.first().recordBody["Name"]) // "Acme Corp"
println(body.records.first().recordBody["AnnualRevenue"]) // 125000000.0 (Double)
⚠️ 注意事项与最佳实践
-
类型安全保障:
readJsonValue()返回Any?,自动映射 JSON 原生类型(String/Int/Double/Boolean/List/Map),避免强制转为String导致数据失真; -
字段顺序无关:
selectName()基于名称匹配,不依赖 JSON 字段排列顺序; -
重复字段防护:显式检查
attributes是否重复,提升错误可读性; -
空值处理:对
attributes和readJsonValue()结果做非空校验,防止静默失败; -
序列化兼容:
toJson方法确保输出 JSON 与输入结构一致,动态字段原样保留; -
性能提示:
JsonReader.Options.of()在编译期生成高效查找表,比字符串if/else更快。
该方案已在真实 Salesforce API 集成场景中稳定运行,兼顾灵活性、健壮性与可维护性——是 Moshi 处理半结构化 JSON 的推荐范式。











