Moshi:解析单个对象或对象列表 (kotlin)
Moshi: Parse single object or list of objects (kotlin)
问题
如何使用 Moshi 从 API 解析单个 Warning
对象或 Warning
对象列表 (List<Warning>
)?
作为单个警告的响应:
{
"warnings": {...}
}
作为警告列表的响应:
{
"warnings": [{...}, {...}]
}
反复试验
试图硬塞一个自动生成的 Moshi 适配器。试图在此基础上构建但失败了。
解决方案
工厂的通用方法
我尝试将 Eric 从 Java 编写的适配器翻译成 Kotlin,因为我意识到更通用的方法更好,就像 Eric 在他的回复中指出的那样。
一旦成功,我将修改此 post 以使其更易于理解。现在有点乱,对不起。
编辑:我最终使用了 Eric 在另一个线程中建议的(翻译成 Kotlin)。
带工厂的适配器
package org.domain.name
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.util.Collections
import java.lang.reflect.Type
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.FIELD
class SingleToArrayAdapter(
val delegateAdapter: JsonAdapter<List<Any>>,
val elementAdapter: JsonAdapter<Any>
) : JsonAdapter<Any>() {
companion object {
val factory = SingleToArrayAdapterFactory()
}
override fun fromJson(reader: JsonReader): Any? =
if (reader.peek() != JsonReader.Token.BEGIN_ARRAY) {
Collections.singletonList(elementAdapter.fromJson(reader))
} else delegateAdapter.fromJson(reader)
override fun toJson(writer: JsonWriter, value: Any?) =
throw UnsupportedOperationException("SingleToArrayAdapter is only used to deserialize objects")
class SingleToArrayAdapterFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<Any>? {
val delegateAnnotations = Types.nextAnnotations(annotations, SingleToArray::class.java) ?: return null
if (Types.getRawType(type) !== List::class.java) throw IllegalArgumentException("Only List can be annotated with @SingleToArray. Found: $type")
val elementType = Types.collectionElementType(type, List::class.java)
val delegateAdapter: JsonAdapter<List<Any>> = moshi.adapter(type, delegateAnnotations)
val elementAdapter: JsonAdapter<Any> = moshi.adapter(elementType)
return SingleToArrayAdapter(delegateAdapter, elementAdapter)
}
}
}
预选赛
注意:我必须添加 @Target(FIELD)
。
@Retention(RUNTIME)
@Target(FIELD)
@JsonQualifier
annotation class SingleToArray
用法
注释要确保被解析为列表的字段 @SingleToArray
。
data class Alert(
@SingleToArray
@Json(name = "alert")
val alert: List<Warning>
)
并将适配器工厂添加到您的 Moshi 实例:
val moshi = Moshi.Builder()
.add(SingleToArrayAdapter.factory)
.build()
参考
- Parse JSON key that is either object or array of object
- https://github.com/square/moshi
the API returns either 1 object if there is only 1 or > 1 a list of objects.
创建一个适配器,它可以查看您是否首先获得了一个数组。
正是您想要的。它包含一个限定符,因此您只能将它应用于可能对单个项目具有此行为的列表。 @SingleToArray List<Warning>
.
还有一个处理多种格式的例子here供进一步阅读。
另一个解决方案
基于 Eric Cochran 的。
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonAdapter.Factory
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonReader.Token.BEGIN_ARRAY
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.lang.reflect.Type
import java.util.Collections.singletonList
import java.util.Collections.emptyList
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.PROPERTY
import org.junit.Assert.assertEquals
@Retention(RUNTIME)
@Target(PROPERTY)
@JsonQualifier
annotation class SingleOrList
object SingleOrListAdapterFactory : Factory {
override fun create(
type: Type,
annotations: Set<Annotation>,
moshi: Moshi
): JsonAdapter<*>? {
val delegateAnnotations = Types.nextAnnotations(annotations, SingleOrList::class.java)
?: return null
if (Types.getRawType(type) !== List::class.java) {
throw IllegalArgumentException("@SingleOrList requires the type to be List. Found this type: $type")
}
val elementType = Types.collectionElementType(type, List::class.java)
val delegateAdapter: JsonAdapter<List<Any?>?> = moshi.adapter(type, delegateAnnotations)
val singleElementAdapter: JsonAdapter<Any?> = moshi.adapter(elementType)
return object : JsonAdapter<List<Any?>?>() {
override fun fromJson(reader: JsonReader): List<Any?>? =
if (reader.peek() !== BEGIN_ARRAY)
singletonList(singleElementAdapter.fromJson(reader))
else
delegateAdapter.fromJson(reader)
override fun toJson(writer: JsonWriter, value: List<Any?>?) {
if (value == null) return
if (value.size == 1)
singleElementAdapter.toJson(writer, value[0])
else
delegateAdapter.toJson(writer, value)
}
}
}
}
class TheUnitTest {
@JsonClass(generateAdapter = true)
internal data class MockModel(
@SingleOrList
val thekey: List<String>
)
@Test
@Throws(Exception::class)
fun testAdapter() {
val moshi = Moshi.Builder().add(SingleOrListAdapterFactory).build()
val adapter: JsonAdapter<List<String>> = moshi.adapter(
Types.newParameterizedType(
List::class.java,
String::class.java),
SingleOrList::class.java
)
assertEquals(adapter.fromJson("[\"Alice\",\"Bob\"]"), listOf("Alice", "Bob"))
assertEquals(adapter.toJson(listOf("Bob", "Alice")), "[\"Bob\",\"Alice\"]")
assertEquals(adapter.fromJson("\"Alice\""), singletonList("Alice"))
assertEquals(adapter.toJson(singletonList("Alice")), "\"Alice\"")
assertEquals(adapter.fromJson("[]"), emptyList<String>())
assertEquals(adapter.toJson(emptyList()), "[]")
}
@Test
fun testDataClassUsage() {
val j1 = """
{
"thekey": "value1"
}
""".trimIndent()
val j2 = """
{
"thekey": [
"value1",
"value2",
"value3"
]
}
""".trimIndent()
val o1 = MockModel::class.java.fromJson(j1, moshi)?.thekey
val o2 = MockModel::class.java.fromJson(j2, moshi)?.thekey
if (o1 != null && o2 != null) {
assertEquals(o1.size, 1)
assertEquals(o1[0], "value1")
assertEquals(o2.size, 3)
assertEquals(o2[0], "value1")
assertEquals(o2[1], "value2")
assertEquals(o2[2], "value3")
}
}
}
问题
如何使用 Moshi 从 API 解析单个 Warning
对象或 Warning
对象列表 (List<Warning>
)?
作为单个警告的响应:
{
"warnings": {...}
}
作为警告列表的响应:
{
"warnings": [{...}, {...}]
}
反复试验
试图硬塞一个自动生成的 Moshi 适配器。试图在此基础上构建但失败了。
解决方案
工厂的通用方法
我尝试将 Eric 从 Java 编写的适配器翻译成 Kotlin,因为我意识到更通用的方法更好,就像 Eric 在他的回复中指出的那样。
一旦成功,我将修改此 post 以使其更易于理解。现在有点乱,对不起。
编辑:我最终使用了
带工厂的适配器
package org.domain.name
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.util.Collections
import java.lang.reflect.Type
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.FIELD
class SingleToArrayAdapter(
val delegateAdapter: JsonAdapter<List<Any>>,
val elementAdapter: JsonAdapter<Any>
) : JsonAdapter<Any>() {
companion object {
val factory = SingleToArrayAdapterFactory()
}
override fun fromJson(reader: JsonReader): Any? =
if (reader.peek() != JsonReader.Token.BEGIN_ARRAY) {
Collections.singletonList(elementAdapter.fromJson(reader))
} else delegateAdapter.fromJson(reader)
override fun toJson(writer: JsonWriter, value: Any?) =
throw UnsupportedOperationException("SingleToArrayAdapter is only used to deserialize objects")
class SingleToArrayAdapterFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<Any>? {
val delegateAnnotations = Types.nextAnnotations(annotations, SingleToArray::class.java) ?: return null
if (Types.getRawType(type) !== List::class.java) throw IllegalArgumentException("Only List can be annotated with @SingleToArray. Found: $type")
val elementType = Types.collectionElementType(type, List::class.java)
val delegateAdapter: JsonAdapter<List<Any>> = moshi.adapter(type, delegateAnnotations)
val elementAdapter: JsonAdapter<Any> = moshi.adapter(elementType)
return SingleToArrayAdapter(delegateAdapter, elementAdapter)
}
}
}
预选赛
注意:我必须添加 @Target(FIELD)
。
@Retention(RUNTIME)
@Target(FIELD)
@JsonQualifier
annotation class SingleToArray
用法
注释要确保被解析为列表的字段 @SingleToArray
。
data class Alert(
@SingleToArray
@Json(name = "alert")
val alert: List<Warning>
)
并将适配器工厂添加到您的 Moshi 实例:
val moshi = Moshi.Builder()
.add(SingleToArrayAdapter.factory)
.build()
参考
- Parse JSON key that is either object or array of object
- https://github.com/square/moshi
the API returns either 1 object if there is only 1 or > 1 a list of objects.
创建一个适配器,它可以查看您是否首先获得了一个数组。
@SingleToArray List<Warning>
.
还有一个处理多种格式的例子here供进一步阅读。
另一个解决方案
基于 Eric Cochran 的
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonAdapter.Factory
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonReader.Token.BEGIN_ARRAY
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.lang.reflect.Type
import java.util.Collections.singletonList
import java.util.Collections.emptyList
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.PROPERTY
import org.junit.Assert.assertEquals
@Retention(RUNTIME)
@Target(PROPERTY)
@JsonQualifier
annotation class SingleOrList
object SingleOrListAdapterFactory : Factory {
override fun create(
type: Type,
annotations: Set<Annotation>,
moshi: Moshi
): JsonAdapter<*>? {
val delegateAnnotations = Types.nextAnnotations(annotations, SingleOrList::class.java)
?: return null
if (Types.getRawType(type) !== List::class.java) {
throw IllegalArgumentException("@SingleOrList requires the type to be List. Found this type: $type")
}
val elementType = Types.collectionElementType(type, List::class.java)
val delegateAdapter: JsonAdapter<List<Any?>?> = moshi.adapter(type, delegateAnnotations)
val singleElementAdapter: JsonAdapter<Any?> = moshi.adapter(elementType)
return object : JsonAdapter<List<Any?>?>() {
override fun fromJson(reader: JsonReader): List<Any?>? =
if (reader.peek() !== BEGIN_ARRAY)
singletonList(singleElementAdapter.fromJson(reader))
else
delegateAdapter.fromJson(reader)
override fun toJson(writer: JsonWriter, value: List<Any?>?) {
if (value == null) return
if (value.size == 1)
singleElementAdapter.toJson(writer, value[0])
else
delegateAdapter.toJson(writer, value)
}
}
}
}
class TheUnitTest {
@JsonClass(generateAdapter = true)
internal data class MockModel(
@SingleOrList
val thekey: List<String>
)
@Test
@Throws(Exception::class)
fun testAdapter() {
val moshi = Moshi.Builder().add(SingleOrListAdapterFactory).build()
val adapter: JsonAdapter<List<String>> = moshi.adapter(
Types.newParameterizedType(
List::class.java,
String::class.java),
SingleOrList::class.java
)
assertEquals(adapter.fromJson("[\"Alice\",\"Bob\"]"), listOf("Alice", "Bob"))
assertEquals(adapter.toJson(listOf("Bob", "Alice")), "[\"Bob\",\"Alice\"]")
assertEquals(adapter.fromJson("\"Alice\""), singletonList("Alice"))
assertEquals(adapter.toJson(singletonList("Alice")), "\"Alice\"")
assertEquals(adapter.fromJson("[]"), emptyList<String>())
assertEquals(adapter.toJson(emptyList()), "[]")
}
@Test
fun testDataClassUsage() {
val j1 = """
{
"thekey": "value1"
}
""".trimIndent()
val j2 = """
{
"thekey": [
"value1",
"value2",
"value3"
]
}
""".trimIndent()
val o1 = MockModel::class.java.fromJson(j1, moshi)?.thekey
val o2 = MockModel::class.java.fromJson(j2, moshi)?.thekey
if (o1 != null && o2 != null) {
assertEquals(o1.size, 1)
assertEquals(o1[0], "value1")
assertEquals(o2.size, 3)
assertEquals(o2[0], "value1")
assertEquals(o2[1], "value2")
assertEquals(o2[2], "value3")
}
}
}