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.
Related
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
)
I followed this guide and found this question for handling network exceptions with Retrofit. While they both are very useful, none of them explain how to extract the body of a successful request. Here is a snippet of code that might better explain what I'm trying to accomplish:
The sealed class remains the same from both examples...
// exception handling:
sealed class NetworkResponse<out T : Any, out U : Any> {
data class Success<T : Any>(val body: T) : NetworkResponse<T, Nothing>()
data class ApiError<U : Any>(val body: U, val code: Int) : NetworkResponse<Nothing, U>()
data class NetworkError(val error: IOException) : NetworkResponse<Nothing, Nothing>()
data class UnknownError(val error: Throwable) : NetworkResponse<Nothing, Nothing>()
}
but extracting the body on a successful call is unclear to me. Here is my attempt:
val oAuthRetrofit: Retrofit = Retrofit.Builder()
.baseUrl(OAUTH_BASE_URL)
.addConverterFactory(Json.asConverterFactory(contentType))
.build()
val salesforceOAuthAPI: SalesforceInterface =
oAuthRetrofit.create(SalesforceInterface::class.java)
val oAuthAccessTokenResponse: NetworkResponse<OAuthAccessTokenResponse, Error> =
salesforceOAuthAPI.getOAuthAccessToken(
GRANT_TYPE,
BuildConfig.SALESFORCE_CLIENT_ID,
BuildConfig.SALESFORCE_CLIENT_SECRET,
BuildConfig.SALESFORCE_USERNAME,
BuildConfig.SALESFORCE_PASSWORD + BuildConfig.SALESFORCE_SECURITY_TOKEN
)
val body = oAuthAccessTokenResponse.body // Unresolved reference
// Use body()
How can I extract the body of a successful request?
In the medium post you have access to the Source Code there you can check what you missing.
Answering your question
How can I extract the body of a successful request?
You can simply do this :
val response1 = salesforceOAuthAPI.getWhatever()
when (response1) {
is NetworkResponse.Success -> Log.d(TAG, "Success ${response1.body.name}")
is NetworkResponse.ApiError -> Log.d(TAG, "ApiError ${response1.body.message}")
is NetworkResponse.NetworkError -> Log.d(TAG, "NetworkError")
is NetworkResponse.UnknownError -> Log.d(TAG, "UnknownError")
}
And in the NetworkResponse.Success you have the data you want, but since you are using a sealed class to detect the error and the data, you don't know what is inside that object until you do a when.
By the way, don't know if you followed all the post but you missing the
.addCallAdapterFactory(NetworkResponseAdapterFactory())
In the Retrofit.Builder() otherwise it won't do the magic to encapsulate everything to a NetworkResponse
Create new class for response model like BaseResponse
data class BaseResponse (
val message: String? = "",
val code: Int? = 0,
)
When request success pass the value of retrofit response to generic class you use
NetworkResponse.Success(response)
and post this value to livedata & observe this livedata on view.
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)
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)
}
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]