【问题标题】:Moshi with duplicate fields具有重复字段的 Moshi
【发布时间】:2020-01-05 21:42:43
【问题描述】:

在某些情况下,我的 json 响应看起来像这样:

{
      "id" : 12345,
      "events": [
         {
            "desc": "Bla bla"
             ...
         }, 
         {
            "desc": "Yada yada",
            ...
         },
       ]

    }

而对于其他一些场景,它看起来像这样:

{
  "id" : 12345,
  "events": {
     "desc": "Bla bla"
     ...
  },
  "events" : {
    "desc": "Yada yada"
    ...
  },
}

也就是说,有时events 会是一个数组,有时events 会与多个值重复。使用 moshi + 改造会引发以下异常:

    2019-12-30 13:58:20.458 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp: Multiple values for 'events' at $[0].events
2019-12-30 13:58:20.458 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp: com.squareup.moshi.JsonDataException: Multiple values for 'events' at $[0].events
2019-12-30 13:58:20.458 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at com.squareup.moshi.kotlin.reflect.KotlinJsonAdapter.fromJson(KotlinJsonAdapter.kt:80)
2019-12-30 13:58:20.458 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:40)
2019-12-30 13:58:20.458 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at com.squareup.moshi.CollectionJsonAdapter.fromJson(CollectionJsonAdapter.java:76)
2019-12-30 13:58:20.458 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at com.squareup.moshi.CollectionJsonAdapter$2.fromJson(CollectionJsonAdapter.java:53)
2019-12-30 13:58:20.458 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:40)
2019-12-30 13:58:20.458 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at retrofit2.converter.moshi.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:45)
2019-12-30 13:58:20.458 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at retrofit2.converter.moshi.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:27)
2019-12-30 13:58:20.458 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:225)
2019-12-30 13:58:20.458 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at retrofit2.OkHttpCall.execute(OkHttpCall.java:188)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at retrofit2.adapter.rxjava2.CallExecuteObservable.subscribeActual(CallExecuteObservable.java:45)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Observable.subscribe(Observable.java:11194)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at retrofit2.adapter.rxjava2.BodyObservable.subscribeActual(BodyObservable.java:34)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Observable.subscribe(Observable.java:11194)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.observable.ObservableSingleSingle.subscribeActual(ObservableSingleSingle.java:35)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Single.subscribe(Single.java:3096)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.single.SingleFlatMap$SingleFlatMapCallback.onSuccess(SingleFlatMap.java:84)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.single.SingleDoOnSuccess$DoOnSuccess.onSuccess(SingleDoOnSuccess.java:59)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.single.SingleFromCallable.subscribeActual(SingleFromCallable.java:56)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Single.subscribe(Single.java:3096)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.single.SingleDoOnSuccess.subscribeActual(SingleDoOnSuccess.java:35)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Single.subscribe(Single.java:3096)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.single.SingleFlatMap.subscribeActual(SingleFlatMap.java:36)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Single.subscribe(Single.java:3096)
2019-12-30 13:58:20.459 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.single.SingleMap.subscribeActual(SingleMap.java:34)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Single.subscribe(Single.java:3096)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.single.SingleFlatMap.subscribeActual(SingleFlatMap.java:36)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Single.subscribe(Single.java:3096)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.single.SingleDoOnSuccess.subscribeActual(SingleDoOnSuccess.java:35)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Single.subscribe(Single.java:3096)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.single.SingleToFlowable.subscribeActual(SingleToFlowable.java:37)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Flowable.subscribe(Flowable.java:13234)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Flowable.subscribe(Flowable.java:13180)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.flowable.FlowableZip$ZipCoordinator.subscribe(FlowableZip.java:127)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.flowable.FlowableZip.subscribeActual(FlowableZip.java:79)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Flowable.subscribe(Flowable.java:13234)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.flowable.FlowableMap.subscribeActual(FlowableMap.java:38)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Flowable.subscribe(Flowable.java:13234)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.flowable.FlowableOnErrorReturn.subscribeActual(FlowableOnErrorReturn.java:33)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Flowable.subscribe(Flowable.java:13234)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.Flowable.subscribe(Flowable.java:13180)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.operators.flowable.FlowableSubscribeOn$SubscribeOnSubscriber.run(FlowableSubscribeOn.java:82)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.schedulers.ScheduledRunnable.run(ScheduledRunnable.java:66)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at io.reactivex.internal.schedulers.ScheduledRunnable.call(ScheduledRunnable.java:57)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at java.util.concurrent.FutureTask.run(FutureTask.java:266)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
2019-12-30 13:58:20.460 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
2019-12-30 13:58:20.461 21419-21419/com.myapp.android.debug E/DetailFragment$loadUp:     at java.lang.Thread.run(Thread.java:764)

我想对这两种场景的输出进行标准化,也就是我想把这个变成

data class Parcel(val events: List<Event>)

我知道第二种响应格式不正确,但我无法控制后端(它是我正在使用的外部服务),有没有办法解决这个问题?

我试着摆弄自定义适配器,但我不知道该怎么做:(

编辑:我对自定义适配器的最佳尝试:

class CorreosApiParcelAdapter(private val eventAdapter: JsonAdapter<CorreosApiEvent>,
                              private val errorAdapter: JsonAdapter<Error>) : JsonAdapter<CorreosApiParcel>() {
    override fun fromJson(reader: JsonReader): CorreosApiParcel? = with(reader) {
        val events = mutableListOf<CorreosApiEvent>()
        val parcel = CorreosApiParcel.allNull()
        beginObject()
        while (hasNext()) {
            val nextName = nextName()
            if (nextName != "eventos" && nextName != "error") {
                val value = reader.nextString()
                when (nextName) {
                    "codEnvio" -> parcel.codEnvio = value
                    "refCliente" -> parcel.refCliente = value
                    "codProducto" -> parcel.codProducto = value
                    "fecha_calculada" -> parcel.fechaCalculada = value
                    "largo" -> parcel.largo = value
                    "ancho" -> parcel.ancho = value
                    "alto" -> parcel.alto = value
                    "peso" -> parcel.peso = value
                }
                continue
            }
            if (nextName == "error") {
                val error = errorAdapter.fromJson(reader)
                if (error != null) {
                    parcel.error = error
                }
                continue
            }

            if (peek() == JsonReader.Token.BEGIN_OBJECT) {
                val fromJson = eventAdapter.fromJson(reader)
                if (fromJson != null) {
                    events += fromJson
                }
                continue
            }
            beginArray()
            while (hasNext()) {
                val fromJson = eventAdapter.fromJson(reader)
                if (fromJson != null) {
                    events += fromJson
                }
            }
            endArray()

        }
        endObject()
        return parcel
    }


    override fun toJson(writer: JsonWriter, value: CorreosApiParcel?) {
    }

    companion object {
        val FACTORY: JsonAdapter.Factory = Factory { type, _, moshi ->


            if (Types.getRawType(type) != CorreosApiParcel::class.java)
                return@Factory null

            val eventAdapter = moshi.adapter<CorreosApiEvent>(CorreosApiEvent::class.java)
            val errorAdapter = moshi.adapter<Error>(Error::class.java)
            return@Factory CorreosApiParcelAdapter(eventAdapter, errorAdapter)
        }
    }
}

【问题讨论】:

标签: android json moshi


【解决方案1】:

Moshi 不支持多个字段,因此您必须为其实现自定义 JsonAdapter。使用fromJson(),您可以访问JsonReader,您可以使用它来确定events 是对象还是数组。

override fun fromJson(reader: JsonReader) = with(reader) {
    val events = mutableList<Event>()
    beginObject()
    while (hasNext()) {
        if (nextName() != "events") { 
            skipValue()
            continue
        }
        if (peek() == BEGIN_OBJECT) {
            events += eventAdapter.fromJson(reader)
            continue
        }
        beginArray()
        while(hasNext()) {
            events += eventAdapter.fromJson(reader)
        }
        endArray()
    }
    endObject()
    Parcel(events)
}

该实现将所有名为 events 的属性读取为对象或数组,具体取决于提供的对象。所有其他属性都被跳过,因为它们与Parcel 无关。 eventAdapter 是您必须提供的依赖项。它负责从json中读取Event对象。

【讨论】:

  • 为了简单起见,我实际上让 Parcel 对象只包含事件,但是我如何才能为对象的其余字段(即字符串和另一个对象)使用“默认”适配器?跨度>
  • 最简单的方法是自己实现它。只需设置值而不是 skipValue()。查看生成的适配器以获取指导。这很简单。否则,您可以创建一个没有events 属性的实体,并在自定义实现之前使用peekJson() 读取数据。
  • 感谢您的帮助,一直在尝试创建这样的自定义适配器,但我正在努力让它工作。我编辑了原始问题以反映我最近的尝试,但我得到了一个错误,因为我的 JsonAdapter.Factory 从未被询问它是否可以支持 CorreosApiParcel,只有 List。此外,如果我将来添加更多字段,我必须明确放置每个字段,这很糟糕:(
  • 用 moshi 做自定义适配器真的很难,我也没有看到任何好的文档
  • CollectionJsonAdapter 应该处理列表并请求您的适配器。可能另一个适配器已经处理了您的类型,因此永远不会询问您的适配器。此外,您可能还想将事件添加到包裹中。
【解决方案2】:

就我而言,Moshi 和 Retrofit 本身并不支持它,所以答案是否定的。

这是无法与后端团队协调的事情吗?如果您尝试并设法在 Android 端处理它,那将会非常复杂。我现在能看到的唯一解决方法是使用您自己的实现进行后续方法调用,该方法将验证并解析 JSON 字符串为有意义的数据类型。比如var events: List&lt;Event&gt;

曾经讨论过这种类型的响应是否允许作为 JSON 文本格式标准化的一部分。

您可以在此处阅读更多信息:

Does JSON syntax allow duplicate keys in an object?

【讨论】:

  • 它是我正在使用的外部服务,所以我完全无法控制响应。糟透了,但我必须自己处理。
  • 是的。你真的必须自己处理它。如果您有时间,并且如果 moshi 的作者发现它有帮助,您可能需要在他们的 repo 中提交 PR 或功能请求。同时,您可以手动处理解析 JSON 字符串。
猜你喜欢
  • 2019-03-27
  • 2018-03-10
  • 1970-01-01
  • 1970-01-01
  • 2016-07-12
  • 1970-01-01
  • 1970-01-01
  • 2018-05-02
  • 1970-01-01
相关资源
最近更新 更多