Set dynamic id as post body - android

I have to send a JSON body similar to this to the API:
"users": {
"{id}": {
"someProperty": "This is some text"
}
}
Where {id} is a dynamic id that I must set before sending the request. (fe. "1", "2", ...)
How can I define a GSON object for this?
data class Test(
#SerializedName("users")
val users: Users
)
data class Users(
// WHAT TO WRITE HERE?
)
I was thinking on using Map but I don't know if it's the right way
data class Test(
#SerializedName("users")
val users: Map<String, User>
)
data class User(
#SerializedName("someProperty")
val someProperty: String
)

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.

Handling two different Retrofit responses for success and error in Kotlin

I'm building an android app in Kotlin, and the response comes in two different structures for success and error. My question is, can I combine those two responses together and create a data class, or should I create a sealed class for that?
Success response:
{
"access_token": "xxxxxxxxxxxxxxxxxxxx",
"token_type": "bearer"
}
Error response:
{
"detail": [
{
"loc": [
"body"
],
"msg": "Invalid credentials",
"type": "invalid-credentials"
}
]
}
So can I write it all like this in a single data class or use a sealed class?
data class Auth (
val access_token: String,
val token_type: String,
val details: Details
)
Yes try combining all the data into a single data class. Just make all the variables optional.
data class Auth (
val access_token: String?,
val token_type: String?,
val detail: List<Detail>?
)
Edit: On how to access the nested data. Pretty Simple.
data class Detail (
val loc: List<String>,
val msg: String,
val type: String
)
Then when an error occurs you access the data with something like val msg = auth.detail?.msg

Handling Errror Response with Moshi

in my Android app, after sending some registration credentials I get the following JSON output from the server:
{
"response":"successfully registered new user",
"email":"testing#gmail.com",
"username":"testing",
"id":9,
"token":"98d26160e624a0b762ccec0cb561df3aeb131ff5"
}
I have modeled this using the Moshi library with the following data class:
#JsonClass(generateAdapter = true)
data class Account (
#Json(name = "id")
val account_id : Long,
#Json(name="email")
val account_email: String,
#Json(name="username")
val account_username: String,
#Json(name="token")
val account_authtoken : String,
#Json(name="response")
val account_response : String
)
Everything works fine. Now I wanted to handle error cases. When I get an error (let's say, the email I want to register with already exists) then I should get a JSON output like this:
// what the app gets when there is some error with the credentials
// e.g. email exists, username exists etc.
{
"error_message" : "The email already exists",
"response": "Error"
}
The method that executes the request looks like the following:
override suspend fun register(email: String, userName: String, password: String, passwordToConfirm: String): NetworkResult<Account> {
// make the request
val response = authApi.register(email, userName, password, passwordToConfirm)
// check if response is successful
return if(response.isSuccessful){
try {
// wrap the response into NetworkResult.Success
// response.body() contains the Account information
NetworkResult.Success(response.body()!!)
}
catch (e: Exception){
NetworkResult.Error(IOException("Error occurred during registration!"))
}
} else {
NetworkResult.Error(IOException("Error occurred during registration!"))
}
}
If the response is successful, then it wraps the response.body() into NetworkResult.Success data class.
My NetworkResult class is a sealed class with two sub data classes Success & Error.
It looks like this:
// I get the idea for this from https://phauer.com/2019/sealed-classes-exceptions-kotlin/
sealed class NetworkResult<out R> {
data class Success<out T>(val data: T) : NetworkResult<T>()
data class Error(val exception: Exception) : NetworkResult<Nothing>()
}
But that does not handle the JSON output for errors I mentioned above. When the app gets the error JSON output, Moshi complains that the Account data class does not have a error_message property which is clear to me because I do not have such a field in my Account data class.
What do I need to change so that I can also handle any error cases I wish ? I know, I could model a second data class and call it Error with the fields response and error_message but my sealed class NetworkResult only accepts one class as generic type.
So, what can I do ?
If you don't initialise a value to a field in data class, Moshi will consider it as a required field.
#JsonClass(generateAdapter = true)
data class Account (
#Json(name = "id")
val account_id : Long = 0,
#Json(name="email")
val account_email: String = "",
#Json(name="username")
val account_username: String = "",
#Json(name="token")
val account_authtoken : String = "",
#Json(name="response")
val account_response : String = "",
#Json(name="error_message")
val error_message : String = ""
)
Like this you can create the same data class for Success and Error both.

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.

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