How to make base class accept generics which have different key - android

I am getting a response something like this where status, message & data keys will remain the same across the different API responses & only the content inside the data JSON object will change, in this example, I have a JSON object with a member key in other response I can have tournament key.
{
"status": true,
"message": "Success",
"data": {
"member": {
"id": 21,
"emailAddress": "abc#xyz.com",
"firstName": "ABC"
}
}
}
Currently, I am making use of generics and doing something like this
data class SignInResponse(
val `data`: Data<Member>
) : BaseResponse()
and BaseResponse class has common JSON keys that I am getting, Here I'm using generics in Data class passing the JSON class that is changing.
open class BaseResponse {
val status: Boolean = false
val message: String = UNDEFINED
}
#Keep
data class Data<T>(val actualData: T)
But this approach is incomplete because the above code will expect a JSON key actualData but here JSON key can be a member , tournament , or anything. How can I pass the class in Data class so that it can support the above JSON response?

Is this as a result from a network call? I've never used generics in trying to parse incoming network calls before.
I'd have a baseData class that contains any common fields between member or tournament - like id or name and then subclass that with a concrete implementation for each possibility which would hold the unique data.
then your BaseResponse class could just be
data class BaseResponse(
val status: Boolean
val message: String
val data: BaseData
)

One way to represent this would be via a class hierarchy rather than generics, because this extra wrapping is not present in the JSON, nor in your conceptual data structure.
A sealed class is what I would usually go for in this case.
However, it all depends on which JSON serialization library you're using. If you control both ends of the communication, I would definitely go for Kotlinx Serialization, which supports sealed hierarchies (and polymorphism in general) in a nice way.
If you don't control the serialization side, maybe a temporary union of all fields as #Chris mentioned is your simplest option.

I ended up using the following approach, where I create a generic for data class
data class BaseResponse<T>(
val status: Boolean, val message: String, val data: T
)
Above implementation enables me to pass model of changing JSON Object inside the data field
data class SignInResponse(
val token: String, val member: Member
)
and after summing up everything we can use it like this
interface BasicxBookingServices {
#POST(Urls.SIGNIN_URL)
suspend fun signIn(#Body body: SignInRequest): Response<BaseResponse<SignInResponse>>
}

Related

Retrofit + GSON + Room Parse subobject as one string

I'm using an API to retrieve some data, and store them in my app DB using Room, Retrofit2, and GSON.
My data object is as follow:
#Entity(tableName = "department")
data class Department(
val nom: String,
#PrimaryKey val code: String,
val region: String
)
And this is what the API returns me
{
"nom": "Ain",
"code": "01",
"region": {
"code": "84",
"nom": "Auvergne-Rhône-Alpes"
}
}
I want to transform the response region.nom as the data region field. My actual solution is to make an interface object that can store the response, then a function for mapping this interface to my data object. But i'm pretty sure there is better/cleaner solution to achieve this (like maybe TypeConverter, but can't understand how it works).
Thanks :
Assume your retrofit api response object name "response".You can simply do this :
var department = Department(response.nom,
response.region.nom)
Then just pass the "department" object to room db insert function.

How to generate dynamic json object for request body

In the Request body below, the number of value "questionOne", "questionTwo", etc changes for each student. How can i dynamically generate request body to fit the changing value of the key and value.
Sample request one
"quiz": {
"name":"Jacob",
"sid": "STD_500",
"questionOne":"",
"questionTwo":""
}
Sample request two
"quiz": {
"name":"Annie",
"sid": "STD_200",
"questionOne":"",
"questionTwo":""
"questionThree":"",
"questionFour":""
}
Data class:
data class Quiz (
val name : String?,
val sid : String?,
val questions: HashMap<String, String>?
)
I suppose the only way would be to define quiz as being a HashMap instead of a Quiz object.
I'm guessing you now have a RequestBody somewhere something like this?
data class RequestBody(
val quiz: Quiz
)
Then change it to
data class RequestBody(
val quiz: HashMap<String,String>
)
But it's kind of a bad design like this, I suggest to work out with the backend a solution as proposed by Tornike's answer
From your description, this is a bad design decision from backend side
You should have one parameter questions on which you will pass list of Question classes like this
First create a separate data class Question
data class Question (
val key:String,
val value:String)
than set list of this data class as Type of questions parameter in a request model like this
data class Quiz (
val name : String?,
val sid : String?,
val questions:List<Question>
)
I'm assuming you are using Gson library for converting data classes to json and vice versa
Solution for given situation is to create Separate request models for each number of questions you send to BE,
BUT i would strongly advise not to do this and make backend guys to change how your api works
The questions should be in a json array. Example:
"quiz": {
"name":"Jacob",
"sid": "STD_500",
"questions" : [
{"key": "questionOne", "value": ""},
{"key": "questionTwo", "value": ""},
]
}

Verify if POJO data class was mapped correctly

I am using a POJO data class with GSON to parse the data which is being called from the Firestore database.
For example, I have a POJO class with few non-nullable, and nullable values like userID
data class Users(id:String="", userID:String="" ...)
I am then using GSON to parse the JSON data to object for that class
val gson = Gson()
val jsonObjects = gson.toJson(querySnapshot.data)
val parseData = gson.fromJson(jsonObjects,Users::class.java)
My question is if someone uploads data in the database and forgets to add the userID (i.e. it is null), is there a way I can check if the User data class is valid when being parsed?
I am using a check like if(userID == ""){return false} . But as the number of non-nullable fields grows it gets tedious and there must be a better way to check this.
My question is if someone uploads data in the database and forgets to add the userID (i.e. it is null), is there a way I can check if the User data class is valid when being parsed?
If you don't want to have null values at all, why would you then let the user the possibility to provide incomplete data? The simplest solution would be to restrict the data that is added to the database. How? Simply by creating some constraints. For example, your users cannot get access to a feature in your app if they do not fill in all the required fields. That's an operation that is widely used.
If you already have null values, then checking against nullity is a solution you can go ahead with. In Kotlin, null safety can be handled in many ways, either by checking for null in conditions, using safe calls, elvis operator or even using the !! operator.
Maybe the extension method of Kotlin is an accepted approach.
Let me show a demo, I assume the class User looks like this:
data class User(
val id: String,
val userId: String? // nullable
)
Create a extension method User.isValid() to verify the fields you want.
fun User.isValid() = when {
userId.isNullOrEmpty() -> false
else -> true
}
The method isNullOrEmpty() is in stdlib of Kotlin. I test the code, it works fine.
val user1 = User("id 001", null)
val user2 = User("id 002", "userId 001")
println(user1.isValid()) //==> print false
println(user2.isValid()) //==> print true
Now, back to your worry point:
...But as the number of non nullable fields grow it gets tedious
I changed the class User
data class User(
val id: String,
val userId: String?,
val email: String?,
val name: String?
)
it means that when the fields of userId, email, name, any of them is null, the User is invalid.
Just add conditions in extention method, like this:
fun User.isValid() = when {
userId.isNullOrEmpty()
|| email.isNullOrEmpty()
|| name.isNullOrEmpty() -> false
else -> true
}
We just need to maintain the method of isValid.
conclusion
Kotlin Extension Method can be used in your case.
It's better don't use id="", also can create an extension method for this empty string if need.
fun String.Companion.empty() = ""
data class User(
val id: String,
val userId: String? = String.empty()
...
)
All the extension methods can be placed in a class, like UserExt.kt for easy maintaining.

Kotlin Deserialization - JSON Array to multiple different objects

I'm using the 1.0.0 version of kotlin serialization but I'm stuck when I try to deserialize a "flexible" array.
From the Backend API that I don't control I get back an JSON Array that holds different types of objects. How would you deserialize them using kotlin serialization?
Example
This is the API's response
[
{
"id": "test",
"person": "person",
"lastTime": "lastTime",
"expert": "pro"
},
{
"id": "test",
"person": "person",
"period": "period",
"value": 1
}
]
#Serializable
sealed class Base {
#SerialName("id")
abstract val id: String
#SerialName("person")
abstract val person: String
}
#Serializable
data class ObjectA (
#SerialName("id") override val id: String,
#SerialName("title") override val title: String,
#SerialName("lastTime") val lastTime: String,
#SerialName("expert") val expert: String
) : Base()
#Serializable
data class ObjectB (
#SerialName("id") override val id: String,
#SerialName("title") override val title: String,
#SerialName("period") val period: String,
#SerialName("value") val value: Int
) : Base()
Performing the following code result in an error
println(Json.decodeFromString<List<Base>>(json))
error Polymorphic serializer was not found for class discriminator
When you say you don't control the API, is that JSON being generated from your code by the Kotlin serialization library? Or is it something else you want to wrangle into your own types?
By default sealed classes are handled by adding a type field to the JSON, which you have in your objects, but it's a property in your Base class. In the next example it shows you how you can add a #SerialName("owned") annotation to say what type value each class corresponds to, which might help you if you can add the right one to your classes? Although in your JSON example both objects have "type" as their type...
If you can't nudge the API response into the right places, you might have to write a custom serializer (it's the deserialize part you care about) to parse things and identify what each object looks like, and construct the appropriate one.
(I don't know a huge amount about the library or anything, just trying to give you some stuff to look at, see if it helps!)
#cactustictacs solution came very close. He said that "By default sealed classes are handled by adding a type field to the JSON"
But because I didn't had a type property I needed a other field that decides which subclass it should be.
In Kotlin Serializer you can do that by
val format = Json {
classDiscriminator = "PROPERTY_THAT_DEFINES_THE_SUBCLASS"
}
val contentType = MediaType.get("application/json")
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(format.asConverterFactory(contentType))
.build()
where in classDiscriminator you can enter the property that you want. Hope this helps other people in the future.

How to implement polymorphic list deserialization with GSON in Kotlin?

I have a data feed that is returning a list that could be either of three types ( Say type A, B and C ). All of the types above share 2 properties, the rest of the properties is specific to the type. I tried using the pattern.
abstract class Parent (val type: String, val id: String)
And
data class TypeA(override val type: String ... )
data class TypeB(override val type: String ... )
I am using Retrofit and trying to deserialize the list to
List<? extends Parent>
which in Kotlin should be
List<out Parent>
However GSON is throwing a deserializing error on instantiating the parent class which is abstract.
java.lang.RuntimeException: Failed to invoke public com.XX.Parent() with no args
Any ideas how I can implement this in Kotlin?
As you have Moshi tagged in your question, I'll give you a way of doing it using MOshi's PolymorphicJsonAdapterFactory. You can basically parse something into different types, depending on the value of the object's property.
First thing you'll do is declared your parent type as a sealed class and have the other types extend from it:
sealed class Parent(val type: String){
data class TypeA(override val type: String, ... ): Parent(type)
data class TypeB(override val type: String, ... ): Parent(type)
}
now you're gonna tell Moshi how to parse Parent objects. You do that registering a PolymorphicJsonAdapterFactory:
val moshi = Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Parent::class.java, "type")
.withSubtype(TypeA::class.java, "typeA")
.withSubtype(TypeB::class.java, "typeB")
.build()
with that, if the value of the property "type" is "typeA", it will deserialize into a TypeA instance. like wise to TypeB, if property "type" is "typeB"
You can look another example here:
https://github.com/square/moshi/blob/master/adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.java

Categories

Resources