If your application uses REST API, there’s a great possibility that you came into contact with JSON. To digest data carried by fetched JSON documents, you have a plethora of libraries. There’s Jackson, Gson, and the coolest kid in town, Moshi. In this blog post, I won’t go into what Moshi is or what makes it stand out from its predecessors. I assume that you have some experience and you know the inner workings. If not, please refer to the official documentation.

Two JSON data parsing scenarios 

With that said, let’s focus on a problem that we’re challenged with. Normally, when parsing JSON data, you can end up with two scenarios: the data structure is correct and can be processed further or it’s no good because a field is missing, the expected type doesn’t match, you name it and you treat it as an error. This applies to a single element response such as user data or a list of elements like products in stock. While in the former, you are pretty much cooked. Of course, you can have some kind of fallback object in the upper layer. When dealing with list elements, you can be more clever about it. Let’s imagine that only some of these elements are bad, but most of them are OK and can be just used as is.

So, let’s refer to that store with product examples, and define objects as

data class Store(
    val name: String,
    val products: List<Product>,
    val employees: List<Employee>
)
data class Product(
    val name: String,
    val price: Double
)

Naturally, it’s represented by

{
  "name": "My Store",
  "products": [
    {
      "name": "Apple",
      "price": "12.34"
    },
    {
      "name": "Orange",
      "price": "56.78"
    }
  ],
  "employees": []
}

We make the following restrictions for product:

  • The name must be a non-null string.
  • The price must be a non-null number where floating point is accepted.

On that note, what can go bad?

  • Name is null.
  • Price in null.
  • Price is not a number.

These spoiled items should be filtered out, and the rest products must be in the resulting list.

With all the requirements laid down, why don’t we wrap it up with code, and what’s a better way to do it than with TDD. Code can be found in repository.

We have failed tests with our custom adapter being, for now, just an empty shell. This is expected.

Now, on to the implementation of the adapter.

class SkipBadListItemJsonAdapter<T : Any> private constructor(
    private val itemJsonAdapter: JsonAdapter<T>
) : JsonAdapter<List<T>>() {

    override fun fromJson(reader: JsonReader): List<T> {
        val elements = mutableListOf<T>()
        reader.beginArray()
        while (reader.hasNext()) {
            val peekedReader = reader.peekJson()
            val element: T? = try {
                itemJsonAdapter.fromJson(peekedReader)
            } catch (jsonDataException: JsonDataException) {
                null
            }

            if (element != null) {
                elements.add(element)
            }
            reader.skipValue()
        }
        reader.endArray()
        return elements
    }

    override fun toJson(writer: JsonWriter, value: List<T>?) {
        writer.beginArray()
        value?.forEachIndexed { index, _ ->
            itemJsonAdapter.toJson(writer, value[index])
        }
        writer.endArray()
    }

    companion object {

        val INSTANCE: Factory = Factory { type, annotations, moshi ->
            if (annotations.isEmpty() && Types.getRawType(type) == List::class.java) {
                val elementType = Types.collectionElementType(type, List::class.java)
                SkipBadListItemJsonAdapter(moshi.adapter(elementType))
            } else {
                null
            }
        }
    }
}

I will focus on fromJson:

  1. Create an empty list of elements that will be returned.
  2. Begin reading array.
  3. Iterate through objects.
  4. Set reader at the beginning of each object.
  5. Try to read JSON element, return object, or null on failed deserialization.
  6. skipValue() allows the user to proceed to the next element in case there was a problem with parsing the current element. Without it, the adapter would fall into a loop.

If you bring your attention to the factory method, you can see that this adapter will be used to handle JSON arrays only.

        val INSTANCE: Factory = Factory { type, annotations, moshi ->
            if (annotations.isEmpty() && Types.getRawType(type) == List::class.java) {
                val elementType = Types.collectionElementType(type, List::class.java)
                SkipBadListItemJsonAdapter(moshi.adapter(elementType))
            } else {
                null
            }
        }

Just an adapter implementation is not sufficient. To put it into work, it has to be added to a parent adapter.

private val adapter: JsonAdapter<Store> = Moshi
        .Builder()
        .add(SkipBadListItemJsonAdapter.INSTANCE)
        .build()
        .adapter(Store::class.java)

This is pretty much it. So now, when SkipBadListItemAdapter will be added, there will be no exception thrown that’s a result of a problem with parsing the JSON array element.

How does Moshi work with parsing?

This all-or-nothing solution may not be suitable for every situation. What if only some JSON related objects with arrays should be safe? Well, in fact, there’s quite a simple solution that takes advantage of yet another Moshi goodie. That’s @JsonQualifier.

Please refer to documentation to learn about the details, but in a nutshell, it allows the user to be more specific about how Moshi operates when parsing objects or event object attributes.

We have to start with creating our own annotation.

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class SkipBadListItemQualifier

We also have to modify the adapter code. The factory method to be specific.

        val INSTANCE: Factory = Factory { type, annotations, moshi ->
            Types.nextAnnotations(annotations, SkipBadListItemQualifier::class.java) ?: return@Factory null
            if (Types.getRawType(type) != List::class.java) {
                throw IllegalArgumentException("Only lists can be annotated with @SkipBadListItemQualifier. Found: $type")
            }
            val elementType = Types.collectionElementType(type, List::class.java)
            SkipBadListItemJsonAdapter(
                itemJsonAdapter = moshi.adapter(elementType)
            )
        }

Now, only fields annotated with @SkipBadListItemQualifier will be parsed using our adapter.

To cover this with tests, our store also now has a list of employees where the problem with parsing this attribute just fails on store object deserialization.

Short summary

There you have it. Now, with this custom adapter, you’re not restricted to two scenarios when processing JSON data from REST Api. That’s to say, either the data is ok, or with a slight hiccup in one of the elements, you’re forced to present the user with an error.

Are you eager for knowledge? More you can find here: