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

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.

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

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.

Error parsing json retrofit android array

I have this json.
{
"people":
[
{ "id":0, "person":true },
{ "id":1, "person":true }
]
}
Model
class Person(var id: Int, var person: Boolean)
My endpoint request
#GET("/posts")
fun getPeople(): Call<List<Person>>
And after request I have failure response and message
java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $
The issue is that you need to create a wrapper around the list itself that contains the people property.
So you should have something like
data class PeopleList(
var people: List<Person>
)
which should then be used in your service definition like
#GET("/posts")
fun getPeople(): Call<PeopleList>
The reason for this is that retrofit is expects a JSON list [...], but the data being sent is a JSON object {...} that contains a list. So you need to tell retrofit how to get to the list.

Android App crashes as Json element is empty String ("") and not Object

I am working on an android project and using RxAndroid, Retrofit to make API call and retrieve json. The json looks something like following :
{
"result": [
{
"parent": "jhasj",
"u_deviation": "skasks",
"caused_by": "ksks",
"u_owner_mi": {
"link": "https://gddhdd.service-now.com/api/now/v1/table/sys_user/ghtytu",
"value": "ghtytu"
},
"impact": "",
}
]
}
I am using gson to parse the Json. The problem is "u_owner_mi" sometimes reruns empty string "" when there is no value assigned to it. I don't have access to change the return type to null. This is making my app crash as I am expecting an object here.
I get the following error :
Expected BEGIN_OBJECT but was STRING
If you can't modify the server, try replacing the offending line in the server response before passing it to the Gson parser. Something like:
String safeResponse = serverResponse.replace("\"u_owner_mi\": \"\"", "\"u_owner_mi\": null");
Your app (client) code is expecting an object according to a contract specified in the class that you pass to GSON. Your app behaves as it should and crashes loudly. You should consider having your server return "u_owner_mi" : null instead of an empty string, assuming you have control over that. The u_owner_mi field on the client side would have to be a nullable type.
If you don't have the ability to fix the api, you could also write a custom deserializer.
Suppose your result class and sub-object are:
data class Result(
val parent: String,
val owner: Any?
)
data class Owner(
val link: String,
val value: String
)
The deserializer could be:
class ResultDeserializer : JsonDeserializer<Result> {
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): Result {
val jsonObject = json.asJsonObject
val ownerProperty = jsonObject.get("owner")
return Result(
parent = jsonObject.get("parent").asString,
owner = if (ownerProperty.isJsonObject) context?.deserialize<Owner>(ownerProperty.asJsonObject, Owner::class.java)
else ownerProperty.asString
)
}
}
Finally, to add the deserializer:
#Test
fun deserialization() {
val gson = GsonBuilder().registerTypeAdapter(Result::class.java, ResultDeserializer()).create()
val result1 = gson.fromJson<Result>(jsonWithObject, Result::class.java)
val result2 = gson.fromJson<Result>(jsonWithEmpty, Result::class.java)
}

Categories

Resources