How to create a right JSON class in Kotlin (for Fuel) - android

I have a request that returns JSON:
{
"success": 0,
"errors": {
"phone": [
"Incorrect phone number"
]
}
}
I plugged Fuel instead of Retrofit for Kotlin. So, my classes are:
data class RegistrationResponse(
val success: Int,
val errors: RegistrationErrorsResponse?) {
class Deserializer : ResponseDeserializable<RegistrationResponse> {
override fun deserialize(content: String): RegistrationResponse? =
Gson().fromJson(content, RegistrationResponse::class.java)
}
}
data class RegistrationErrorsResponse(val phone: List<String>?) {
class Deserializer : ResponseDeserializable<RegistrationErrorsResponse> {
override fun deserialize(content: String): RegistrationErrorsResponse? =
Gson().fromJson(content, RegistrationErrorsResponse::class.java)
}
}
A request looks like:
class Api {
init {
FuelManager.instance.basePath = SERVER_URL
}
fun registration(name: String, phone: String): Request =
"/registration/"
.httpPost(listOf("name" to name, "phone" to phone))
}
private fun register(name: String, phone: String) {
Api().registration(name, phone)
.responseObject(RegistrationResponse.Deserializer()) { _, response, result ->
val registrationResponse = result.component1()
if (registrationResponse?.success == 1) {
showScreen()
} else {
showErrorDialog(registrationResponse?.errors?.phone?.firstOrNull())
}
}
}
A problem is that when error occurs, phone variable in data class (registrationResponse?.errors?.phone) is filled with null, but not "Incorrect phone number".

After searching through Fuel issues I understood that in most cases we don't need to write own deserializators as they are already written by Gson.
In https://github.com/kittinunf/Fuel/issues/265 there is an example. So, just put your data class inside <>:
URL.httpPost(listOf("name" to name, "phone" to phone)).responseObject<RegistrationResponse> ...
and get data through
result.component1()?.errors?.phone?.firstOrNull()
Old version of answer
Probably an obstacle is a list deserialization, see
1. https://github.com/kittinunf/Fuel/issues/233 and
2. https://github.com/kittinunf/Fuel/pull/236.
I think, by default Fuel doesn't use Gson deserialization.
I still don't know how to deserialize a list, But got values with this expression:
((result.component1().obj()["errors"] as JSONObject).get("phone") as JSONArray)[0]

Related

More efficient way to model JSON data using retrofit - Kotlin

The data that I want to use has this structure:
{
"1": {
"id": 1,
"name": "Bulbasaur"
},
"2": {
"id": 2,
"name": "Ivysaur"
},
"3": {
"id": 3,
"name": "Venusaur"
}
}
Note:
The number labeling each object matches the id of the Pokémon, not the number of Pokémon
My problem is that when I try to create data classes for this it ends up creating a data class for each object. Not one data class that fits each object. I believe this is due to the number labeling the object(Pokémon) being different for each object.
Is there a way I can format this data in maybe one or two data classes and not over 800?
Ideally I would like the data to be structured like this but it does not work when run.
data class ReleasedPokemonModel(
val id: Int,
val name: String
)
When parsing Json to Object with this special case, you should custom Json Deserializer yourself.
Here I use Gson library to parse Json to Object.
First, create a custom Json Deserializer with Gson. As follows:
PokemonResponse.kt
data class PokemonResponse(
val pokemonMap: List<StringReleasedPokemonModel>
)
data class ReleasedPokemonModel(
val id: Int,
val name: String
)
GsonHelper.kt
object GsonHelper {
fun create(): Gson = GsonBuilder().apply {
registerTypeAdapter(PokemonResponse::class.java, PokemonType())
setLenient()
}.create()
private class PokemonType : JsonDeserializer<PokemonResponse> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): PokemonResponse {
val list = mutableListOf<ReleasedPokemonModel>()
// Get your all key
val keys = json?.asJsonObject?.keySet()
keys?.forEach { key ->
// Get your item with key
val item = Gson().fromJson<ReleasedPokemonModel>(
json.asJsonObject[key],
object : TypeToken<ReleasedPokemonModel>() {}.type
)
list.add(item)
}
return PokemonResponse(list)
}
}
}
Next I will create a GsonConverterFactory so that I can addConvertFactory to Retrofit.
val gsonConverterFactory = GsonConverterFactory.create(GsonHelper.create())
And now I will add retrofit.
val retrofit = Retrofit.Builder()
// Custom your Retrofit
.addConverterFactory(gsonConverterFactory) // Add GsonConverterFactoty
.build()
Finally in ApiService, your response will now return type PokemonResponse.
interface ApiService {
#GET("your_link")
suspend fun getGenres(): PokemonResponse
}
The problem is that there's no JSON array there. it's literally one JSON object with each Pokemon listed as a property. I would recommend that you reformat the JSON beforehand to look like this:
[
{
"id": 1,
"name": "Bulbasaur"
},
{
"id": 2,
"name": "Ivysaur"
},
{
"id": 3,
"name": "Venusaur"
}
]
And then you could model it like this:
data class ReleasedPokemonModel(
val id: Int,
val name: String
)
data class Response(
val items: List<ReleasedPokemonModel>
)
See more here.
And see here for discussion about reformatting the data before handing it to Retrofit.
You can use Map to store the key like the following
data class PokemonResponse(
val pokemonMap:Map<String,ReleasedPokemonModel>
)
data class ReleasedPokemonModel(
val id: Int,
val name: String
)

Kotlin Enum to Basic Type mapping

I have a login API, which accepts that a field, type is either "email" or "phone".
I'd like to make them Enums
enum class LoginBodyType (val value: String) {
EMAIL("email"), PHONE ("phone")
}
I have a data class similar to follows
class LoginBody (val type: LoginBodyType) {
var phone: String = ""
var email: String = ""
var password: String = ""
}
so that when I call the login function, it will be something like this
val body = LoginBody(LoginBodyType.EMAIL)
body.email = username
body.password = password
where the type will be fixed to one of the enums, but it would submit to the server as a type String.
One option is to do body = LoginBody(LoginBodyType.EMAIL.value) and class LoginBody (val type: String) but it feels like there's a more elegant solution. I'm using Retrofit 2 if that matters.
You could try to play around with sealed class. So something like:
enum class LoginBodyType {
EMAIL,
PHONE
}
sealed class LoginBody {
data class Email(val email: String, val password: String)
data class Phone(val phone: String)
}
val body = when(type) {
LoginBodyType.EMAIL -> LoginBody.Email(email, pass)
LoginBodyType.PHONE -> LoginBody.Phone(phone)
}
Alternatively check How to obtain all subclasses of a given sealed class?
You could do something like this.
enum class LoginBodyType {
EMAIL,
PHONE;
override fun toString(): String = name.toLowerCase()
}
val body = LoginBody(LoginBodyType.EMAIL)

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.

Parsing boolean from Firebase Database Snapshot

I'm having issues pasing a Snapshot from the Firebase Realtime Database into a Data Class in Kotlin using ProGuard.
Here's how the data looks in the Firebase console:
Here's how I've modeled that data class In my android app:
data class PickupCode(
val code: String,
val boxId: String,
val orderId: String,
val suborderId: String,
val drawers: List<Int>,
val isDelivered: Boolean
) {
constructor(): this("", "", "","", emptyList(), false)
override fun toString(): String {
return code
}
}
Here's how I build the database request:
val reference = database.getReference("pickupCodes/$boxId/$code")
val listener = object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.exists()) {
println(snapshot)
val pickupCode = snapshot.getValue<PickupCode>(PickupCode::class.java)
pickupCode?.let {
println("Code: ${it.code}, is delivered: ${it.isDelivered} to drawers: ${it.drawers.toString()}")
if (!it.isDelivered) {
// No success
} else {
// Success!
}
} ?: run {
// No success
}
} else {
// No success
}
}
override fun onCancelled(error: DatabaseError) {
// No success
}
}
This is what the println(snapshot) line prints:
DataSnapshot { key = 320625, value = {isDelivered=true, code=320625, drawers={0=2}, orderId=-LhdzXS4-gyT0ysNe-zi, suborderId=-LhdzYhT78y9b3iJcyrb, boxId=box_1} }
And this is what the next print 3 lines lates prints:
Code: 320625, is delivered: false to drawers: [2]
Here I would expect is delivered to be true, but for some reason true-value of isDelivered from the snapshot, is ignored when parsing the snapshot into a PickupCode-class. The value isDelivered of the PickupCode, is equal to the empty constructor of the class.
But WHY and HOW to fix?
All the other values from the snapshot gets parsed corrently. I'm new to Android, but I have a hunch that ProGuard (whatever that is) has some of the blame here.. Here's how I've set it up:
-keepattributes Signature
-keepclassmembers class PickupCode.** {
*;
}
I found a solution to this..
When inspecting the verbose logs I found this:
W/ClassMapper: No setter/field for isDelivered found on class com.x.y.models.PickupCode
After playing a bit around, I found that for some weird reason, the setters for properties starting with is are ignored :S I tested with other property names as well and types.. fx. val isBerp: Number gets the same warning.
So after changing the property name from isDelivered to delivered in both the class and firebase, it works..
I wasn't able to find documentation for this behaviour, so if someone knows about it, it would appreciate a link..

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