I am trying to read some images from firestore collection using retrofit into my app
This is how my JSON looks like
"documents": [
{
"name": "projects/la370210/databases/(default)/documents/Photos/ahCinu5bJVwe",
"fields": {
"img_url": {
"stringValue": "https://firebasestorage.googleapis.com/v0/b/pq-villa-370210.appspot.com/o/PQPhotos%2Fsitting_area_anex.jpg?alt=media&token=0d02643e-b69d-4031-b51f-"
},
"img_des": {
"stringValue": "Annex hall"
}
},
"createTime": "2022-12-01T05:14:58.593903Z",
"updateTime": "2022-12-01T05:14:58.593903Z"
},
...]
What I want is to get the value for the stringValue for 'img_url' and 'img_des' to my data class variables.
My data class
#Serializable
data class Photos (
#SerialName(value = "img_url")
val imgUrl: List<String>,
#SerialName(value = "img_des")
val imgDes: List<String>,
)
The Api interface
`
interface PQApiService {
#GET("PQPhotos")
suspend fun getPhotos(): List<PQPhotos>
}
`
In my DefaultAppContainer I have
`
private val BASE_URL =
"https://firestore.googleapis.com/v1/projects/$PQ_PROJECT_ID/databases/(default)/documents/"
private val retrofit = Retrofit.Builder()
.addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
.baseUrl(BASE_URL)
.build()
`
I am trying to consume and emit JSON which contains a polymorphic list of items. The problem is: the items contain type key with integer values (not strings). The API endpoint produces and expects JSON similar to this:
{
"startTime": "2022-07-27T13:32:57.379Z",
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
},
{
"type": 2,
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
}
],
"results": "string"
}
],
"status": 0,
"clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
As described in the article on polymorphism, I created an hierarchy of classes. I also try to convert type value before deserialization.
object MyTransformingDeserializer : JsonTransformingSerializer<BaseItem>(PolymorphicSerializer(BaseItem::class)) {
override fun transformDeserialize(element: JsonElement): JsonElement {
val type = element.jsonObject["type"]!!
val newType = JsonPrimitive(value = type.toString())
return JsonObject(element.jsonObject.toMutableMap().also { it["type"] = newType })
}
}
#Serializable(with = MyTransformingDeserializer::class)
sealed class BaseItem {
abstract val type: String
}
#Serializable
#SerialName("0")
class ItemType0(
override val type: String,
// ...
) : BaseItem()
#Serializable
#SerialName("1")
class ItemType1(
override val type: String,
// ...
) : BaseItem()
#Serializable
#SerialName("2")
class ItemType2(
override val type: String,
// ...
) : BaseItem()
But all I get is this error:
kotlinx.serialization.json.internal.JsonDecodingException: Polymorphic
serializer was not found for class discriminator '0'
Given that I can not change the format of the JSON, what can be done to successfully serialize/desereialize it?
Handling polymorphism in Kotlinx Serialization is difficult, especially when you don't have control over the format of the source. But KxS does give a lot of low-level tools to manually handle almost anything.
You were close in choosing JsonTransformingSerializer! It seems that it doesn't transform the JSON before KxS selects a serializer. Because discriminators can only be strings, and so deserialization fails.
JsonContentPolymorphicSerializer
Instead of JsonTransformingSerializer, you can use JsonContentPolymorphicSerializer.
Kotlinx Serialization will first deserialize the JSON to a JsonObject. It will then provide that object to the serializer for BaseItem, and you can parse and select the correct subclass.
import kotlinx.serialization.*
import kotlinx.serialization.json.*
object BaseItemSerializer : JsonContentPolymorphicSerializer<BaseItem>(BaseItem::class) {
override fun selectDeserializer(
element: JsonElement
): DeserializationStrategy<out BaseItem> {
return when (val type = element.jsonObject["type"]?.jsonPrimitive?.intOrNull) {
0 -> ItemType0.serializer()
1 -> ItemType1.serializer()
2 -> ItemType2.serializer()
else -> error("unknown Item type $type")
}
}
}
Including type
Since this is manually performing polymorphic discrimination, there's no need to include type in your classes.
import kotlinx.serialization.Serializable
#Serializable(with = BaseItemSerializer::class)
sealed class BaseItem
#Serializable
data class ItemType0(
// ...
) : BaseItem()
#Serializable
class ItemType1(
// ...
) : BaseItem()
#Serializable
class ItemType2(
// ...
) : BaseItem()
However you might like to include it, for completeness, and so it's included when serializing. For that, you must use #EncodeDefault
import kotlinx.serialization.EncodeDefault
import kotlinx.serialization.Serializable
#Serializable(with = BaseItemSerializer::class)
sealed class BaseItem {
abstract val type: Int
}
#Serializable
class ItemType0(
// ...
) : BaseItem() {
#EncodeDefault
override val type: Int = 0
}
// ...
Complete example
Bringing it all together, here's a complete example.
import kotlinx.serialization.*
import kotlinx.serialization.json.*
val mapper = Json {
prettyPrint = true
prettyPrintIndent = " "
}
fun main() {
val json = """
{
"startTime": "2022-07-27T13:32:57.379Z",
"status": 0,
"clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
},
{
"type": 2,
"items": [
{
"type": 0,
"results": "string",
"restBetweenRounds": "string"
},
{
"type": 1,
"results": "string",
"setCount": 0
}
],
"results": "string"
}
]
}
""".trimIndent()
val itemHolder: ItemHolder = mapper.decodeFromString(json)
println(itemHolder)
println(mapper.encodeToString(itemHolder))
}
#Serializable
data class ItemHolder(
val startTime: String,
val clientId: String,
val status: Int,
val items: List<BaseItem>,
)
#Serializable(with = BaseItem.Serializer::class)
sealed class BaseItem {
abstract val type: Int
object Serializer : JsonContentPolymorphicSerializer<BaseItem>(BaseItem::class) {
override fun selectDeserializer(
element: JsonElement
): DeserializationStrategy<out BaseItem> {
return when (val type = element.jsonObject["type"]?.jsonPrimitive?.intOrNull) {
0 -> ItemType0.serializer()
1 -> ItemType1.serializer()
2 -> ItemType2.serializer()
else -> error("unknown Item type $type")
}
}
}
}
#Serializable
data class ItemType0(
val results: String,
val restBetweenRounds: String,
) : BaseItem() {
#EncodeDefault
override val type: Int = 0
}
#Serializable
data class ItemType1(
val results: String,
val setCount: Int,
) : BaseItem() {
#EncodeDefault
override val type: Int = 1
}
#Serializable
data class ItemType2(
val results: String,
val items: List<BaseItem>,
) : BaseItem() {
#EncodeDefault
override val type: Int = 2
}
This prints
ItemHolder(
startTime=2022-07-27T13:32:57.379Z,
clientId=3fa85f64-5717-4562-b3fc-2c963f66afa6,
status=0,
items=[
ItemType0(results=string, restBetweenRounds=string),
ItemType1(results=string, setCount=0),
ItemType2(
results=string,
items=[
ItemType0(results=string, restBetweenRounds=string),
ItemType1(results=string, setCount=0)
]
)
]
)
and
{
"startTime": "2022-07-27T13:32:57.379Z",
"clientId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": 0,
"items": [
{
"results": "string",
"restBetweenRounds": "string",
"type": 0
},
{
"results": "string",
"setCount": 0,
"type": 1
},
{
"results": "string",
"items": [
{
"results": "string",
"restBetweenRounds": "string",
"type": 0
},
{
"results": "string",
"setCount": 0,
"type": 1
}
],
"type": 2
}
]
}
which matches the input!
Versions
Kotlin 1.7.10
Kotlinx Serialization 1.3.4
Given an ObjectA that contains a List of ObjectB from a WebClient request. I'm wanting to return a flow of my DTO from ObjectB
data class MyDto(val id: String, val name: String) {
companion object {
fun from(objectB: ObjectB) {
return MyDto(id = objectB.id, name = objectB.name)
}
}
...
return webClient.get()
.uri(baseUrl)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToFlow<ObjectA>()
.flatMapConcat { response ->
val x = response.objectB.map { MyDto.from(it) }
retrun#flatMapConcat flowOf(x)
}
This returns JSON of Array<Array<MyDto>>
[
[
{
"id": 1,
"name": "Joe"
},
{
"id": 2,
"name": "Jane"
}
]
]
Expected results are Flow<MyDto>
[
{
"id": 1,
"name": "Joe"
},
{
"id": 2,
"name": "Jane"
}
]
flowOf(x) returns Flow<List<ObjectA>>, but you need to return Flow<ObjectA>
...
.flatMapConcat { response ->
response.objectB.asFlow().map { MyDto.from(it) }
}
How to make GroupBy result list to new Map in Webflux
There is my input list and expect result. then what should I do to make the result.
// expect
{
"timestamp": "2019-06-13T00:00:00.000",
"result": {
"first": 1,
"second": 2,
"third": 3
}
}
// input list
[
{
"timestamp": "2019-06-13T00:00:00.000",
"first": 1
},
{
"timestamp": "2019-06-13T00:00:00.000",
"second": 2
},
{
"timestamp": "2019-06-13T00:00:00.000",
"third": 3
}
]
val Flux.fromIterable(list)
.groupBy{it.timestamp}
.concatMap { groupItem ->
// here!! I want to make `group by result list to new Map``
Result(timestamp = groupItem.key()!!, Item(first = ?, second = ?, third =?))
}
I figured it out.
Flux.merge(first, second, thrid)
.groupBy { it.timestamp }
.concatMap {
it.map { item ->
val container = mutableMapOf<String, Any>()
if (item is firstEntity) {
container["first"] = item.result.count
container["timestamp"] = it.key()!!
}
if (item is secondEntity) container["second"] = item.result.count
if (item is thridEntity) container["thrid"] = item.result.count
container
}.reduce { acc, current ->
acc.putAll(current)
acc
}
}
.map {
val first = (it["first"] ?: 0) as Int
val second = (it["second"] ?: 0) as Int
val thrid = (it["thrid"] ?: 0) as Int
val timestamp = (it["timestamp"] ?: "") as String
// somthing!!
}
I am using Vk Api, and during 1 scenario I am getting a response object with code 200, but body of it's an Error JSON.
I want to ask you - is it possible to somehow get the error object from response and look at the error_code that has been returned from the Vk Api.
I am using Retrofit 2 on android and GsonConverterFactory.
I am trying to do something like this:
class NetworkCheckerInterceptor(val networkChecker: NetworkChecker) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
if (!networkChecker.isConnected()) {
throw NoNetworkConnectionException("No network connection.")
}
try {
val response = chain.proceed(requestBuilder.build())
val error = Gson().fromJson(response.body()?.string(), Error::class.java)
return response
} catch (e: SocketTimeoutException) {
throw SocketTimeoutException()
} catch (e: UnknownHostException) {
throw UnknownHostException()
}
}
}
But I am getting error when I am trying to get 'Error' object.
Json error example:
{
"error": {
"error_code": 7,
"error_msg": "Permission to perform this action is denied",
"request_params": [
{
"key": "oauth",
"value": "1"
},
{
"key": "method",
"value": "stats.get"
},
{
"key": "timestamp_to",
"value": "1542549195"
},
{
"key": "month",
"value": "month"
},
{
"key": "group_id",
"value": "56461699"
},
{
"key": "v",
"value": "5.87"
},
{
"key": "interval",
"value": "month"
},
{
"key": "stats_groups",
"value": "visitors"
},
{
"key": "timestamp_from",
"value": "1514757660"
}
]
}
}
The only thing I care about is "error_code": 7 it's about permition problem.
So, how can I get this object even if my response code is 200 ?
You can create base class for VK network response
abstract class BaseVkResponse<T> {
var response: T? = null
var error: VKError? = null // (from vk sdk)
}
and each response should extend it. For example
class NewsItem {
var type: String? = null
var text: String? = null
var date: Long? = null
}
class NewsPage {
var items: List<NewsItem>? = null
#SerializedName("nextFrom")
var nextFrom: String? = null
}
class NewsResponse : BaseVkResponse<NewsPage>()
and retrofit interface looks like
interface VkService {
#GET("newsfeed.getRecommended")
fun getNews(#Query("access_token") accessToken: String,
#Query("v") apiVersion: String,
#Query("count") count: Int?,
#Query("start_from") startFrom: String?): Single<NewsResponse>
}
Then register special type adapter to parse VkError type
internal class ErrorTypeAdapter : JsonDeserializer<VKError> {
#Throws(JsonParseException::class)
override fun deserialize(json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): VKError? {
return try {
VKError(JSONObject(json.toString()))
} catch (e: JSONException) {
null
}
}
}
val gson = GsonBuilder().apply {
registerTypeAdapter(VKError::class.java, ErrorTypeAdapter())
}
If response's error field is not null, you should handle it as you wish. In other cases you can treat it as successful.