
本文介绍在 Moshi 中优雅处理“部分字段固定 + 其余字段动态”的 JSON 结构,通过自定义 @FromJson/@ToJson 适配器实现 attributes 固定解析 + 其余字段统一映射到 Map,规避 Moshi 原生不支持 @JsonAnySetter 的限制。
本文介绍在 moshi 中优雅处理“部分字段固定 + 其余字段动态”的 json 结构,通过自定义 `@fromjson`/`@tojson` 适配器实现 `attributes` 固定解析 + 其余字段统一映射到 `map
Moshi 作为轻量、高性能的 Kotlin/Java JSON 库,虽支持 @JsonClass(generateAdapter = true) 自动生成类型安全的适配器,但不提供类似 Jackson 的 @JsonAnySetter 注解。当面对 Salesforce 等 API 返回的典型结构(如 attributes 固定嵌套对象 + 其他业务字段动态扩展)时,需手动编写适配器来分离解析逻辑。
以下是一个健壮、可复用的解决方案,核心在于利用 JsonReader.Options 高效识别已知字段,并将其余字段交由 reader.readJsonValue() 统一解析为 Any 类型:
✅ 定义数据类(保持简洁,无需注解干扰)
@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>
✅ 编写自定义 RecordAdapter(关键实现)
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"
}]
}
""".trimIndent()
val body = bodyAdapter.fromJson(json)
println(body.records.first().recordBody["Name"]) // 输出: Acme Corp
println(bodyAdapter.toJson(body)) // 可逆序列化,结构完全还原
⚠️ 注意事项与最佳实践
-
readJsonValue()是关键:它递归解析任意 JSON 类型(对象、数组、字符串、数字、布尔、null)为 Kotlin 对应的Any(如Map<string any></string>、List<any></any>、String、Double、Boolean、null),确保动态字段类型保真。 -
字段顺序无关:
selectName()不依赖 JSON 中字段出现顺序,比“假设attributes在首位”更可靠。 -
空值与重复校验:显式检查
attributes是否重复、是否为 null,提升错误提示友好性。 -
序列化需传入
JsonAdapter<any></any>:Moshi 会自动选择内置的AnyJsonAdapter,无需手动构造。 -
性能考量:该方案避免反射和泛型擦除问题,比
@JsonQualifier+Map<string jsonelement></string>更直接高效。
通过此模式,你可在强类型约束下灵活应对开放结构 API,兼顾安全性与扩展性——这正是 Moshi “类型优先,可控扩展”设计哲学的典型实践。











