Я занимаюсь разработкой приложения для Android, а бэкэнд пишется с использованием Spring Rest Data.
Сценарий:
Допустим, я хочу перечислить любимые продукты пользователя. Мне нужно позвонить GET /v1/users/4/favoriteProducts
с предоставлением токена доступа в заголовках. Если у пользователя есть любимые продукты, я получу ответ, который выглядит следующим образом:
{
"links" : [ {
"rel" : "self",
"href" : "BASE_URL/v1/users/4/favoriteProducts",
"hreflang" : null,
"media" : null,
"title" : null,
"type" : null,
"deprecation" : null
} ],
"content" : [
{PRODUCT},
{PRODUCT},
{PRODUCT},
]
}
И я могу десериализовать это без проблем, я получаю HalList
объект, который содержит три продукта. Мой сервисный вызов Retrofit выглядит следующим образом:
@GET("v1/users/{user}/favoriteProducts")
fun favoriteProducts(@Path("user") userId: Int): Call<HalList<Product>>
и мой HalList
класс:
data class HalList<T>(
@SerializedName("links")
val links: List<HalLink>,
@SerializedName("content")
var content: List<T>
)
но если у меня нет любимых продуктов, я получаю что-то вроде этого:
{
"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" : [ ]
} ]
}
И когда я анализирую это с помощью Retrofit, я получаю HalList
объект с content
, содержащий один экземпляр класса Product со всеми значениями, установленными в 0 или ноль или ложь, в зависимости от типа, и это создает проблему ..
Я написал это TypeAdapter
для Gson, чтобы переопределить содержимое пустым списком, если оно пустое на основе 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
}
}
Но я не уверен, что это правильное решение, и есть ли более элегантное решение? Также есть что-нибудь, что можно сделать на бэкенде, чтобы вернуть пустой список, например так:
{
"links" : [ {
"rel" : "self",
"href" : "BASE_URL/v1/users/4/favoriteProducts",
"hreflang" : null,
"media" : null,
"title" : null,
"type" : null,
"deprecation" : null
} ],
"content" : []
}
РЕДАКТИРОВАТЬ : окончательное решение
Мне не понравилась идея разрешить адаптеру делегата Gson анализировать ответ, а затем я проверяю строку json и использую отражение, чтобы изменить ее на пустой список. Я изменил свой подход, чтобы изменить ответ json перед десериализацией:
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>
)