I am developing Android application and backend is being written using Spring Rest Data.
Scenario:
Let's say I want to list user's favorite products. I need to call GET /v1/users/4/favoriteProducts
providing access token in headers. If user has favoriteProducts, I will get response that looks like this:
{
"links" : [ {
"rel" : "self",
"href" : "BASE_URL/v1/users/4/favoriteProducts",
"hreflang" : null,
"media" : null,
"title" : null,
"type" : null,
"deprecation" : null
} ],
"content" : [
{PRODUCT},
{PRODUCT},
{PRODUCT},
]
}
And I can deserialize this without problems, I get HalList
object which contains three products. My Retrofit service call looks like this:
@GET("v1/users/{user}/favoriteProducts")
fun favoriteProducts(@Path("user") userId: Int): Call<HalList<Product>>
and my HalList
class:
data class HalList<T>(
@SerializedName("links")
val links: List<HalLink>,
@SerializedName("content")
var content: List<T>
)
but if I do not have any favorite products I get something like this:
{
"links" : [ {
"rel" : "self",
"href" : "BASE_URL/v1/users/4/favoriteProducts",
"hreflang" : null,
"media" : null,
"title" : null,
"type" : null,
"deprecation" : null
} ],
"content" : [ {
"rel" : null,
"collectionValue" : true,
"relTargetType" : "com.example.entity.Product",
"value" : [ ]
} ]
}
And when I parse this with Retrofit I get HalList
object with content
containing one instance of Product class with all values set to 0 or null or false, depending on type, and this makes a problem..
I have wrote this TypeAdapter
for Gson in order to override content with empty list if it is empty based on json
class HalTypeAdapterFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>?): TypeAdapter<T> {
val delegate = gson?.getDelegateAdapter(this, type)!!
if (!HalReflection.isResource(type?.rawType!!)) {
return delegate
}
return HalTypeAdapter(gson, type, delegate)
}
}
class HalTypeAdapter<T>(private val gson: Gson, private val type: TypeToken<T>, private val delegate: TypeAdapter<T>) : TypeAdapter<T>() {
private val basicAdapter = gson.getAdapter(JsonElement::class.java)!!
override fun write(out: JsonWriter?, value: T) {
delegate.write(out, value)
}
override fun read(`in`: JsonReader?): T {
val fullJson = basicAdapter.read(`in`)
val deserialized = delegate.fromJsonTree(fullJson)
if(type.rawType == HalList::class.java || type.rawType == HalPagedList::class.java) {
if(fullJson.isJsonObject) {
val content = fullJson.asJsonObject.getAsJsonArray("content")
if(content[0].isJsonObject) {
val o = content[0].asJsonObject
if(o.has("collectionValue") && o.has("relTargetType")) {
val field = type.rawType.getDeclaredField("content")
field.isAccessible = true
field.set(deserialized, listOf<T>())
field.isAccessible = false
}
}
}
}
return deserialized
}
}
But I am not sure if this is the right solution and is there any more elegant solution? Also is there anything that can be done on backend in order to return empty list like this:
{
"links" : [ {
"rel" : "self",
"href" : "BASE_URL/v1/users/4/favoriteProducts",
"hreflang" : null,
"media" : null,
"title" : null,
"type" : null,
"deprecation" : null
} ],
"content" : []
}
EDIT: Final Solution
I did not like the idea to let Gson's delegate adapter to parse response and then I check json string and use reflection to change it to empty list. I changed my approach to modify json response before deserializing it:
class HalTypeAdapterFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>?): TypeAdapter<T> {
val delegate = gson?.getDelegateAdapter(this, type)!!
if (type?.rawType == HalList::class.java)
return HalListTypeAdapter(gson, type, delegate)
return delegate
}
}
class HalListTypeAdapter<T>(
private val gson: Gson,
private val type: TypeToken<T>?,
private val delegate: TypeAdapter<T>)
: TypeAdapter<T>() {
private val basicAdapter = gson.getAdapter(JsonElement::class.java)
override fun write(out: JsonWriter?, value: T) {
delegate.write(out, value)
}
override fun read(`in`: JsonReader?): T {
val json = basicAdapter.read(`in`)
if (json.isJsonObject) {
val resource = json.asJsonObject
val content = resource.getAsJsonArray("content")
if (isEmptyCollection(content)) {
resource.remove("content")
resource.add("content", JsonArray())
}
}
return delegate.fromJsonTree(json)
}
private fun isEmptyCollection(content: JsonArray): Boolean {
if (content.size() == 1 && content[0].isJsonObject) {
val first = content[0].asJsonObject
return first.has("collectionValue") && first.has("relTargetType")
}
return false
}
}
data class HalList<T>(
@SerializedName("links")
val links: List<HalReference>,
@SerializedName("page")
val page: HalListPageMeta?,
@SerializedName("content")
val content: List<T>
)