Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Moshi: Parse single object or list of objects (kotlin)

I am building an app which can fetch a Warning object from an API which I do not control nor do they want to remove this behaivor to which I am trying to work around.

I would like to be able to parse either a single Warning object or a List<Warning> objects. (Because the API returns either 1 object if there is only 1 or > 1 a list of objects.)

Question

How can I parse an object as a list directly or parse list this with Moshi?

The data I get looks either like this:

{
  # List<Warning>
  "warnings": [{...}, {...}]
}

or like this:

{
  # Warning
  "warnings": {...}
}

I have read and found some sources on this topic, but no examples and I am not sure how to proceed.. so any pointers would be highly appreciated.

Attempt

I initially tried to shoehorn an autogenerated moshi adapter thinking I might be able to build ontop of it but I couldn't figure out what was going on so it didn't really lead me anywhere. But I included the generated code anyway incase it might be useful for the task still.

Edit: This is what I currently have, though im not very pleased by the fact it takes an instance of Moshi to work.

Solution

Generalised approach with a Factory

I tried to port adapter which Eric wrote to kotlin since I since has realised that a more general approach is better much like Eric points out in his reply. As soon a this is working, I will re-write parts of this post to make it more understandable, its a bit messy now I apologize for that.

EDIT: I ended up using the solution Eric suggested in another thread but ported to Kotlin.

Adapter with Factory

class SingleToArrayAdapter(
    val delegateAdapter: JsonAdapter<List<Any>>,
    val elementAdapter: JsonAdapter<Any>
) : JsonAdapter<Any>() {

    companion object {
        val INSTANCE = 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)
            if (delegateAnnotations == null) return null
            if (Types.getRawType(type) != List::class.java) throw IllegalArgumentException("Only lists may 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)
        }
    }
}

Qualifier

Note: I had to add the Target(FIELD).

@Retention(RUNTIME)
@Target(FIELD)
@JsonQualifier
annotation class SingleToArray

Usage

Annotate a field you want to make sure is parsed as a list with @SingleToArray.

data class Alert(
    @SingleToArray
    @Json(name = "alert")
    val alert: List<Warning>
)

and dont forget to add the adapter to your moshi instance:

val moshi = Moshi.Builder()
            .add(SingleToArrayAdapter.INSTANCE)
            .build()

Related issues/topics:

  • Parse JSON key that is either object or array of object
  • Moshi Determine if JSON is array or single object
  • https://github.com/square/moshi
like image 550
sphrak Avatar asked Nov 16 '18 19:11

sphrak


Video Answer


1 Answers

the API returns either 1 object if there is only 1 or > 1 a list of objects.

Create an adapter that peeks to see if you're getting an array first. Here is exactly what you want. It includes a qualifier, so you can apply it only to lists that may have this behavior for single items. @SingleToArray List<Warning>.

There is another example of dealing with multiple formats here for further reading.

like image 149
Eric Cochran Avatar answered Sep 20 '22 04:09

Eric Cochran