0

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>
)
clzola
  • 1,925
  • 3
  • 30
  • 49
  • Can you provide your `Product` class – Brijesh Joshi Jul 05 '18 at 10:13
  • You can tell your backend to send response of content as **"content" : [ ]** . For them its possible to send empty array when no favorite product are there. – Nainal Jul 05 '18 at 11:01
  • @BrijeshJoshi implementation of Product class has nothing to do with problem, it is just data class with attributes related to Product Entity – clzola Jul 05 '18 at 11:14
  • @Nainal What should backend do in order to set **"content": []**? – clzola Jul 05 '18 at 11:15
  • @clzola It will be helpful to check for null issue – Brijesh Joshi Jul 05 '18 at 11:15
  • @BrijeshJoshi `null`s (default values) are expected because I do not have `collectionValue`, `relTargetType` in **Product class** as they are not part of **Product Entity**, when list of entities is empty Spring Data Rest returns list with one object that says that list is empty... and when list is not empty, when there are products then everything works fine – clzola Jul 05 '18 at 11:18

0 Answers0