Retrofit + GSON + Room Parse subobject as one string - android

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.

Related

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": ""},
]
}

How to make base class accept generics which have different key

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>>
}

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 can I deserialize this?

I have a JSON response of an API REST call that I am not pretty sure how should I deserialize...
{
.....
"date": "10-10-19",
"rates": {
"GBP" : 101.01,
"EUR" : 102.01,
"AUD" : 103.4,
......
}
}
I would like to know How could I deserialize the "rates" object?. I think it was a Map object so using Gson I make the next POJO:
class POJO(
private val base: String,
private val date: Date,
private val rates: Rate
)
And my Rate class is
class Rate ( private val currency : Map <String, Double> )
It doesn't make any problem unless I try to use this Map in my class. When I try to access to this variable for example here :
view?.converterBinder!!.setCurrencyList(it.data!!.rates.currency)
currency is null because I think Gson doesn't know how to resolve it. I don't know if I had to deserialize it manually or there are any solution for this using Gson.
Any thoughts??
The provided Json is completely wrong, this is how it should be formatted
{
"date": "10-10-19",
"rates": {
"GBP": 101.01,
"EUR": 102.01
}
}
Please check with https://jsonlint.com to confirm the validity of a Json.
So you have a json object with a String "date", then you have another json object called "rates" containing 2 numeric Doubles "GBP" and "EUR".
Each Json should be represented by a class, so to parse it create the following object containing the 2 classes
object Models {
data class Rates(#SerializedName("GBP") val gbp: Double,
#SerializedName("EUR") val eur: Double)
data class ExchangeRates(#SerializedName("date") val date: String,
#SerializedName("rates") val rates: Rates)
}
Now you pass the class ExchangeRates to Gson to deserialize your object and you should have all the data in place.

Retrofit #Patch method (Updating an object, which contains an ArrayList)

Note: Newbie here, please let me know if i need to provide more information or clarify on anything.
To give you some context: I am practising building a Messenger-clone application with lots of Retrofit methods. For that purpose, i am using a small local JSON server, with which the application communicates.
When a user of the application creates an account, the application creates a profile object in the JSON server using the following method:
#FormUrlEncoded
#POST("profiles")
suspend fun createProfile(#Field("username") username: String?,
#Field("picture") picture: String?,
#Field(value = "nickname") nickname: String?,
#Field(value = "contacts") contacts: ArrayList<String?>,
#Field(value = "status") status: Int?): Response<Profile>
Initially, the contacts ArrayList is empty, because the user has not yet added any contacts. Creating a random profile with an empty ArrayList() for the contacts parameter, this is the result inside the JSON server:
{
"username": "username.example",
"picture": "picture's URL",
"nickname": "Nikola",
"status": 1,
"id": 4
}
The class that represents the Profile model inside the application is this:
class Profile(
val username: String? = "",
var picture: String? = "",
var nickname: String? = "",
var contacts: ArrayList<String?>? = ArrayList(),
var status: Int? = 1,
val id: Int? = 0
)
Once the profile is created, naturally the user can add new contacts, which happens using the following method:
#FormUrlEncoded
#PATCH("profiles/{id}")
suspend fun addContact(#Path("id") id: Int?,
#Field("contacts") contacts: ArrayList<String?>?): Response<Profile>
And here is where the problem occurs, on the very first contact added. The ArrayList, which is sent to server contains just one item and the result inside the JSON server looks like this:
{
"username": "username.example",
"picture": "picture's URL",
"nickname": "Nikola",
"status": 1,
"id": 4,
"contacts": "first.contact"
}
Basically, because the arraylist contains just one item, it saves it as a String. This creates all kinds of problems later on because, once the application uses a #GET method for that profile, it expects an ArrayList for the contacts attribute, but it receives a String.
What can i do to make the the JSON profile look like this:
{
"username": "username.example",
"picture": "picture's URL",
"nickname": "Nikola",
"status": 1,
"id": 4,
"contacts": ["first.contact"]
}
The contacts parameter needs to be an array, even when there is only one item in it.
Use #Body instead of #Form and #FormUrlEncoded:
data class ProfileContacts(val contacts: List<String>)
#PATCH("profiles/{id}")
suspend fun addContact(#Path("id") id: Int?, #Body contacts: ProfileContacts): Response<Profile>
and add a converter, if you haven't already had one, a Gson one for example:
// build.gradle
dependencies {
implementation 'com.squareup.retrofit2:converter-gson:2.6.1' // latest version
}
// Retrofit Builder
val retrofit = Retrofit.Builder()
... // other methods
.addConverterFactory(GsonConverterFactory.create())
.build()
#Body lets you define the request body as a Kotlin class, which will eventually get serialized using the provided Converter (in case of Gson, it will be converted to JSON). #Field on the other hand is used for sending data as application/x-www-form-urlencoded (as the required #FormUrlEncoded annotation also suggests). This means that the body of your request will be encoded into a list of key-value pairs, separated by '&', e.g. (based on the createProfile method):
username=username.example&picture=picture%27s%20URL&nickname=Nikola&status=1&id=4
You can POST an array as application/x-www-form-urlencoded by using the same key more than once. That's what basically happens when you annotate a list with the Retrofit #Field annotation - every element from the list is paired with the common key, e.g.:
#FormUrlEncoded
#PATCH("profiles/{id}")
suspend fun addContact(#Path("id") id: Int?,
#Field("contacts") contacts: ArrayList<String?>?): Response<Profile>
// ...
addContact(1, arrayListOf("first.contact", "second.contact"))
// request body:
contacts=first.contact&contacts=second.contact
So when you try to update the profile using only one element contacts list, a single "contacts" pair gets created (contacts=first.contact), and it's treated like a string value.

Categories

Resources