'Moshi: Parse single object or list of objects (kotlin)
Problem
How to parse either a single Warning
object or a list of Warning
objects (List<Warning>
) from an API using Moshi?
The response as a single warning:
{
"warnings": {...}
}
The response as a list of warnings:
{
"warnings": [{...}, {...}]
}
Trial and Error
Tried to shoehorn an autogenerated Moshi adapter. Tried to build on top of it but failed.
Solution
Generalized approach with a factory
I tried to translate the adapter Eric wrote from Java to Kotlin since I realized that a more general approach is better much like Eric points out in his reply.
Once it works, I will revise this post to make it easier to understand. A bit messy now I'm sorry.
EDIT: I ended up using the solution Eric suggested in another thread (translated into Kotlin).
Adapter with factory
package org.domain.name
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.util.Collections
import java.lang.reflect.Type
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.FIELD
class SingleToArrayAdapter(
val delegateAdapter: JsonAdapter<List<Any>>,
val elementAdapter: JsonAdapter<Any>
) : JsonAdapter<Any>() {
companion object {
val factory = 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) ?: return null
if (Types.getRawType(type) !== List::class.java) throw IllegalArgumentException("Only List can 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 add the adapter factory to your Moshi instance:
val moshi = Moshi.Builder()
.add(SingleToArrayAdapter.factory)
.build()
Reference
Solution 1:[1]
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.
Solution 2:[2]
Another Solution
Based on Eric Cochran's Java version solution.
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonAdapter.Factory
import com.squareup.moshi.JsonQualifier
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonReader.Token.BEGIN_ARRAY
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import java.lang.reflect.Type
import java.util.Collections.singletonList
import java.util.Collections.emptyList
import kotlin.annotation.AnnotationRetention.RUNTIME
import kotlin.annotation.AnnotationTarget.PROPERTY
import org.junit.Assert.assertEquals
@Retention(RUNTIME)
@Target(PROPERTY)
@JsonQualifier
annotation class SingleOrList
object SingleOrListAdapterFactory : Factory {
override fun create(
type: Type,
annotations: Set<Annotation>,
moshi: Moshi
): JsonAdapter<*>? {
val delegateAnnotations = Types.nextAnnotations(annotations, SingleOrList::class.java)
?: return null
if (Types.getRawType(type) !== List::class.java) {
throw IllegalArgumentException("@SingleOrList requires the type to be List. Found this type: $type")
}
val elementType = Types.collectionElementType(type, List::class.java)
val delegateAdapter: JsonAdapter<List<Any?>?> = moshi.adapter(type, delegateAnnotations)
val singleElementAdapter: JsonAdapter<Any?> = moshi.adapter(elementType)
return object : JsonAdapter<List<Any?>?>() {
override fun fromJson(reader: JsonReader): List<Any?>? =
if (reader.peek() !== BEGIN_ARRAY)
singletonList(singleElementAdapter.fromJson(reader))
else
delegateAdapter.fromJson(reader)
override fun toJson(writer: JsonWriter, value: List<Any?>?) {
if (value == null) return
if (value.size == 1)
singleElementAdapter.toJson(writer, value[0])
else
delegateAdapter.toJson(writer, value)
}
}
}
}
class TheUnitTest {
@JsonClass(generateAdapter = true)
internal data class MockModel(
@SingleOrList
val thekey: List<String>
)
@Test
@Throws(Exception::class)
fun testAdapter() {
val moshi = Moshi.Builder().add(SingleOrListAdapterFactory).build()
val adapter: JsonAdapter<List<String>> = moshi.adapter(
Types.newParameterizedType(
List::class.java,
String::class.java),
SingleOrList::class.java
)
assertEquals(adapter.fromJson("[\"Alice\",\"Bob\"]"), listOf("Alice", "Bob"))
assertEquals(adapter.toJson(listOf("Bob", "Alice")), "[\"Bob\",\"Alice\"]")
assertEquals(adapter.fromJson("\"Alice\""), singletonList("Alice"))
assertEquals(adapter.toJson(singletonList("Alice")), "\"Alice\"")
assertEquals(adapter.fromJson("[]"), emptyList<String>())
assertEquals(adapter.toJson(emptyList()), "[]")
}
@Test
fun testDataClassUsage() {
val j1 = """
{
"thekey": "value1"
}
""".trimIndent()
val j2 = """
{
"thekey": [
"value1",
"value2",
"value3"
]
}
""".trimIndent()
val o1 = MockModel::class.java.fromJson(j1, moshi)?.thekey
val o2 = MockModel::class.java.fromJson(j2, moshi)?.thekey
if (o1 != null && o2 != null) {
assertEquals(o1.size, 1)
assertEquals(o1[0], "value1")
assertEquals(o2.size, 3)
assertEquals(o2[0], "value1")
assertEquals(o2[1], "value2")
assertEquals(o2[2], "value3")
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Eric Cochran |
Solution 2 | George |