【问题标题】:Moshi Custom Adapter for generic List<T:Enum> returns List<List<T:Enum>> instead of List<T:Enum>通用 List<T:Enum> 的 Moshi 自定义适配器返回 List<List<T:Enum>> 而不是 List<T:Enum>
【发布时间】:2019-05-09 06:16:31
【问题描述】:

(也在这里打开了一个问题:https://github.com/square/moshi/issues/768,但也被要求打开一个 stackoverflow 问题) 我正在编写一个通用适配器来转换带有枚举值列表的 json 字符串。当列表包含不可用的枚举值时,枚举的标准适配器会引发异常。我想创建一个简单地跳过未知枚举值而不是抛出异常的适配器。我部分成功了,但由于某种原因,转换的对象不是List&lt;Enum&gt;,而是List&lt;List&lt;Enum&gt;&gt;。

这是我想出的适配器:

package com.mytestcompany.appname.utils

import com.squareup.moshi.*
import kotlin.reflect.KClass

class SkipNotFoundEnumInEnumListAdapter<T : Enum<T>>(enumType: KClass<T>) : JsonAdapter<List<T>>(){
    val jsonNameToEnum = HashMap<String,T>()
    val enumToJsonName = HashMap<T,String>()

    init{
        val enumConstants =  enumType.java.enumConstants
        for(enumConstant in enumConstants){
            val constantName = enumConstant.name
            val jsonName = enumType.java.getField(constantName).getAnnotation(Json::class.java)

            val lookupName = jsonName?.name ?: constantName
            jsonNameToEnum[lookupName] = enumConstant
            enumToJsonName[enumConstant] = lookupName
        }
    }

    @FromJson
    override fun fromJson(jsonReader: JsonReader): List<T>{
        val list = ArrayList<T>()
        while(jsonReader.hasNext()){
            val jsonNameValue = jsonReader.nextString()
            val entry = jsonNameToEnum[jsonNameValue]
            if(entry!= null){
                list.add(entry)
            }
        }
        return list
    }

    @ToJson
    override fun toJson(writer: JsonWriter, list: List<T>?){
        if(list!=null){
            for(item in list){
                val jsonName = enumToJsonName[item]
                if(jsonName != null){
                    writer.value(jsonName)
                }
            }
        }
    }
}

以及单元测试代码:

package com.mytestcompany.appname.utils

import com.squareup.moshi.Json
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.junit.MockitoJUnitRunner


data class TestJsonClass(
    val testJsonSubClass: TestJsonSubClass
)

data class TestJsonSubClass(
    val tags: List<Tags>
)

enum class Tags {
    @Json(name="tag1") TAG_1,
    @Json(name="tag2") TAG_2,
    @Json(name="tag3") TAG_3,
}

@RunWith(MockitoJUnitRunner::class)
class SkipNotFoundEnumInEnumListAdapterTest {

    lateinit var jsonAdapter: JsonAdapter<TestJsonClass>

    @Before
    fun setUp() {
        val moshi = Moshi.Builder()
            .add(Tags::class.java, SkipNotFoundEnumInEnumListAdapter(Tags::class))
            .add(KotlinJsonAdapterFactory())
            .build()
        jsonAdapter = moshi.adapter(TestJsonClass::class.java)
    }

    @Test
    fun moshiAdapterKnownEnumsTest() {
        val json = """
            {
                "testJsonSubClass": {
                    "tags": [
                        "tag1",
                        "tag2",
                        "tag3"
                    ]
                },
                "validation": {}
            }
        """.trimIndent()

        val testJsonClass = jsonAdapter.fromJson(json)
        Assert.assertTrue(testJsonClass?.testJsonSubClass?.tags?.count() == 3)
    }

    @Test
    fun moshiAdapterUnknownEnumsTest() {
        val json = """
            {
                "testJsonSubClass": {
                    "tags": [
                        "tag1",
                        "tag2",
                        "tag5"
                    ]
                },
                "validation": {}
            }
        """.trimIndent()

        val testJsonClass = jsonAdapter.fromJson(json)
        Assert.assertTrue(testJsonClass?.testJsonSubClass?.tags?.count() == 2)
    }
}

在调试第二个测试的 testJsonClass 对象时,我可以看到以下值(与第一个测试类似):

我认为这与CollectionJsonAdapter有关,因为自定义适配器是通过CollectionJsonAdapter调用的。在我认为我会将集合传递给转换器并编写jsonReader.beginArray() 和reader.endArray() 之前,但这已经为我完成了:

   //in CollectionJsonAdapter.java 
  @Override public C fromJson(JsonReader reader) throws IOException {
    C result = newCollection();
    reader.beginArray();
    while (reader.hasNext()) {
      result.add(elementAdapter.fromJson(reader));    // calls the custom adapter
    }
    reader.endArray();
    return result;
  }

我不确定我能做些什么来解决这个问题,我无法在我的适配器中返回单个值,所以它需要是一个列表,但我也不知道如何强制 moshi 不使用 CollectionJsonAdapter 并通过整个集合到我的适配器。

【问题讨论】:

    标签: generics kotlin moshi


    【解决方案1】:

    将您的适配器注册为处理List&lt;Tags&gt;,而不是Tags。

    .add(Types.newParameterizedType(List::class.java, Tags::class.java),
        SkipNotFoundEnumInEnumListAdapter(Tags::class))
    

    此外,您需要在解码实现中添加 jsonReader.beingArray() 和 jsonReader.endArray() 调用。 (注意,直接扩展JsonAdapter不需要@FromJson。)

    override fun fromJson(jsonReader: JsonReader): List<T> {
      val list = ArrayList<T>()
      jsonReader.beginArray()
      while(jsonReader.hasNext()){
        val jsonNameValue = jsonReader.nextString()
        val entry = jsonNameToEnum[jsonNameValue]
        if(entry!= null){
          list.add(entry)
        }
      }
      jsonReader.endArray()
      return list
    }
    

    奖励:您可以使用 JsonReader.Options 优化 SkipNotFoundEnumInEnumListAdapter。

    class SkipNotFoundEnumInEnumListAdapter<T : Enum<T>>(enumType: Class<T>) : JsonAdapter<List<T>>() {
      private val nameStrings: Array<String>
      private val constants: Array<T>
      private val options: JsonReader.Options
    
      init {
        try {
          constants = enumType.enumConstants
          nameStrings = Array(constants.size) {
            val constant = constants[it]
            val annotation = enumType.getField(constant.name).getAnnotation(Json::class.java)
            annotation?.name ?: constant.name
          }
          options = JsonReader.Options.of(*nameStrings)
        } catch (e: NoSuchFieldException) {
          throw AssertionError("Missing field in " + enumType.name, e)
        }
    
      }
    
      @Throws(IOException::class)
      override fun fromJson(reader: JsonReader): List<T> {
        reader.beginArray()
        val list = mutableListOf<T>()
        while (reader.hasNext()) {
          val index = reader.selectString(options)
          if (index != -1) {
            list += constants[index]
          } else {
            reader.skipValue()
          }
        }
        reader.endArray()
        return list
      }
    
      @Throws(IOException::class)
      override fun toJson(writer: JsonWriter, value: List<T>?) {
        if (value == null) {
          throw IllegalArgumentException("Wrap in .nullSafe()")
        }
        writer.beginArray()
        for (i in value.indices) {
          writer.value(nameStrings[value[i].ordinal])
        }
        writer.endArray()
      }
    }
    

    【讨论】:

    • 非常感谢,注册适配器是个问题。我之前在代码中有 beginArray() 和 endArray() 调用,但注意到 beginArray() 已经被调用(因为注册错误)。
    • 是否有另一种方法来编写上面的代码但避免使用扩展运算符?
    • @eric-cochran 我想知道这是否不应该是 Moshi 对于所有 List 的默认行为,或者至少允许通过 Moshi Builder 上的某个标志来设置它,而不是使用适配器。我在 Github 上创建了一个问题来讨论这个问题:github.com/square/moshi/issues/1118
    猜你喜欢
    • 2022-07-11
    • 2013-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多