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()

参考

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")
        }
    }
}