Moshi adapter to skip bad objects in the List<T>
Asked Answered
G

3

7

I use Moshi and I need to solve my problem with a buggy backend. Sometimes, when I request a list of objects, some of them don't contain mandatory fields. Of course, I can catch and process JsonDataException, but I want to skip these objects. How can I do it with Moshi?

Update

I have a couple of models for my task

@JsonClass(generateAdapter = true)
data class User(
        val name: String,
        val age: Int?
)

@JsonClass(generateAdapter = true)
data class UserList(val list: List<User>)

and buggy JSON

{
  "list": [
    {
      "name": "John",
      "age": 20
    },
    {
      "age": 18
    },
    {
      "name": "Jane",
      "age": 21
    }
  ]
}

as you can see, the second object has no mandatory name field and I want to skip it via Moshi adapter.

Grief answered 11/1, 2019 at 11:22 Comment(2)
Post the code on how you're setting up your Moshi adapter.Mundt
@Mundt I have no code for the adapter. I just want to write it =) But I can show code with models.Grief
G
4

It seems I've found the answer

class SkipBadListObjectsAdapterFactory : JsonAdapter.Factory {
    override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
        return if (annotations.isEmpty() && Types.getRawType(type) == List::class.java) {
            val elementType = Types.collectionElementType(type, List::class.java)
            val elementAdapter = moshi.adapter<Any>(elementType)

            SkipBadListObjectsAdapter(elementAdapter)
        } else {
            null
        }
    }

    private class SkipBadListObjectsAdapter<T : Any>(private val elementAdapter: JsonAdapter<T>) :
        JsonAdapter<List<T>>() {
        override fun fromJson(reader: JsonReader): List<T>? {
            val goodObjectsList = mutableListOf<T>()

            reader.beginArray()

            while (reader.hasNext()) {
                try {
                    elementAdapter.fromJson(reader)?.let(goodObjectsList::add)
                } catch (e: JsonDataException) {
                    // Skip bad element ;)
                }
            }

            reader.endArray()

            return goodObjectsList

        }

        override fun toJson(writer: JsonWriter, value: List<T>?) {
            throw UnsupportedOperationException("SkipBadListObjectsAdapter is only used to deserialize objects")
        }
    }
}

Thank you "guys from the other topics" =)

Grief answered 11/1, 2019 at 18:29 Comment(2)
Consider using JsonReader.peekJson() to get a copy of the current JsonReader that doesn't consume. You can use skip() to skip the full value even if a nested object is malformed.Bryna
I am running into an issue with how this jives with Kotlin generics it would appear. The exception I keep getting is: java.lang.IllegalArgumentException: No JsonAdapter for E (with no annotations). If anyone has a workaround it would be much appreciated! Also, I feel like Moshi should offer this functionality out of the box as an annotation or something. Its pretty common in a lot of other parsers.Sneaking
B
5

There's a gotcha in the solution that only catches and ignores after failure. If your element adapter stopped reading after an error, the reader might be in the middle of reading a nested object, for example, and then the next hasNext call will be called in the wrong place.

As Jesse mentioned, you can peek and skip the entire value.

class SkipBadElementsListAdapter(private val elementAdapter: JsonAdapter<Any?>) :
    JsonAdapter<List<Any?>>() {
  object Factory : JsonAdapter.Factory {
    override fun create(type: Type, annotations: Set<Annotation>, moshi: Moshi): JsonAdapter<*>? {
      if (annotations.isNotEmpty() || Types.getRawType(type) != List::class.java) {
        return null
      }
      val elementType = Types.collectionElementType(type, List::class.java)
      val elementAdapter = moshi.adapter<Any?>(elementType)
      return SkipBadElementsListAdapter(elementAdapter)
    }
  }

  override fun fromJson(reader: JsonReader): List<Any?>? {
    val result = mutableListOf<Any?>()
    reader.beginArray()
    while (reader.hasNext()) {
      try {
        val peeked = reader.peekJson()
        result += elementAdapter.fromJson(peeked)
      } catch (ignored: JsonDataException) {
      }
      reader.skipValue()
    }
    reader.endArray()
    return result

  }

  override fun toJson(writer: JsonWriter, value: List<Any?>?) {
    if (value == null) {
      throw NullPointerException("value was null! Wrap in .nullSafe() to write nullable values.")
    }
    writer.beginArray()
    for (i in value.indices) {
      elementAdapter.toJson(writer, value[i])
    }
    writer.endArray()
  }
}
Britzka answered 14/1, 2019 at 23:25 Comment(1)
This implementation crashes with PolymorphicJsonAdapterFactory when the create is called. com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory.create(PolymorphicJsonAdapterFactory.java:216)Mchail
G
4

It seems I've found the answer

class SkipBadListObjectsAdapterFactory : JsonAdapter.Factory {
    override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
        return if (annotations.isEmpty() && Types.getRawType(type) == List::class.java) {
            val elementType = Types.collectionElementType(type, List::class.java)
            val elementAdapter = moshi.adapter<Any>(elementType)

            SkipBadListObjectsAdapter(elementAdapter)
        } else {
            null
        }
    }

    private class SkipBadListObjectsAdapter<T : Any>(private val elementAdapter: JsonAdapter<T>) :
        JsonAdapter<List<T>>() {
        override fun fromJson(reader: JsonReader): List<T>? {
            val goodObjectsList = mutableListOf<T>()

            reader.beginArray()

            while (reader.hasNext()) {
                try {
                    elementAdapter.fromJson(reader)?.let(goodObjectsList::add)
                } catch (e: JsonDataException) {
                    // Skip bad element ;)
                }
            }

            reader.endArray()

            return goodObjectsList

        }

        override fun toJson(writer: JsonWriter, value: List<T>?) {
            throw UnsupportedOperationException("SkipBadListObjectsAdapter is only used to deserialize objects")
        }
    }
}

Thank you "guys from the other topics" =)

Grief answered 11/1, 2019 at 18:29 Comment(2)
Consider using JsonReader.peekJson() to get a copy of the current JsonReader that doesn't consume. You can use skip() to skip the full value even if a nested object is malformed.Bryna
I am running into an issue with how this jives with Kotlin generics it would appear. The exception I keep getting is: java.lang.IllegalArgumentException: No JsonAdapter for E (with no annotations). If anyone has a workaround it would be much appreciated! Also, I feel like Moshi should offer this functionality out of the box as an annotation or something. Its pretty common in a lot of other parsers.Sneaking
A
0

You can find a working solution here:

https://github.com/square/moshi/issues/1288

Happy fixing :)

Appetence answered 29/11, 2021 at 15:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.