I use Retrofit (v2.9.0) and Moshi (v1.11.0) in my app. I try to call an endpoint this way:
#FormUrlEncoded
#PATCH("anime/{anime_id}/my_list_status")
fun updateListStatus(
#Path("anime_id") animeId: Long,
#Field("num_watched_episodes") nbWatchedEpisodes: Int,
#Field("score") score: Double,
#Field("status") watchStatus: WatchStatus,
): Single<MyListStatus>
But the WatchStatus->Json conversion is not working as expect. WatchStatus is a simple enum class:
enum class WatchStatus {
COMPLETED,
DROPPED,
ON_HOLD,
PLAN_TO_WATCH,
WATCHING,
}
and I created a custom adapter because my app uses uppercase enum names while the back-end uses lowercase names:
class AnimeMoshiAdapters {
/* Others adapters */
#ToJson
fun watchStatusToJson(watchStatus: WatchStatus): String =
watchStatus.toString().toLowerCase(Locale.getDefault())
#FromJson
fun watchStatusFromJson(watchStatus: String): WatchStatus =
WatchStatus.valueOf(watchStatus.toUpperCase(Locale.getDefault()))
}
I create my Moshi instance this way:
Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.add(AnimeMoshiAdapters())
.build()
and my Retrofit instance uses it (with Koin injection):
Retrofit.Builder()
.baseUrl(get<String>(named("baseUrl")))
.client(get(named("default")))
.addConverterFactory(MoshiConverterFactory.create(get()))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
When parsing a Json to create a WatchStatus enum the adapter is used. It is noticeable because the call fails with an error "com.squareup.moshi.JsonDataException: Expected one of [COMPLETED, DROPPED, ON_HOLD, PLAN_TO_WATCH, WATCHING]" if I remove my custom adapter.
When I try to call the endpoint specified above the transformation of a WatchStatus in Json is wrong and the enum name stay in Uppercase, meaning my custom adapter is not used. If I check the Retrofit logs I can see that it send "num_watched_episodes=12&score=6.0&status=ON_HOLD", so the status is not converted in lowercase.
If I try to manually convert a WatchStatus in Json using the same Moshi instance it works as expected, so I believe my custom adapter implementation is correct.
How can I make Retrofit uses my custom Moshi adapter in this call?
Moshi adapters' toJson apply to the retrofit requests' body (== params annotated with #BODY), not query parameters (== params annotated with #FIELD). And it is correct and expected behavior, as query parameters are by standards not expected to be JSON formatted strings (eventhough in your case it's just a String). If your server expects the status field as query parameter, then there is no other way than to provide it lowerCased by yourself:
#FormUrlEncoded
#PATCH("anime/{anime_id}/my_list_status")
fun updateListStatus(
...
#Field("status") watchStatus: String
): Single<MyListStatus>
and feed your updateListStatus with already lowerCased value:
updateListStatus(..., COMPLETED.name.toLowerCase())
If you have no influence on the server's implementation, then skip the rest of this post.
If you want to utilize your custom adapter's toJson function, your server needs to change the request to accept JSON body instead of query params, say like this:
PUT: anime/{anime_id}/my_list_status
BODY:
{
"anime_id" : Long,
"num_watched_episodes" : Int,
"score" : Double,
"status" : WatchStatus
}
Then you would create a data class for the body, say named RequestBody, then you could change your request to:
#PUT("anime/{anime_id}/my_list_status")
fun updateListStatus(
...
#BODY body: RequestBody
): Single<MyListStatus>
in which case your custom adapter will take effect and transform the WatchStatus inside the RequestBody by its defined toJson logic.
Related
I am trying to parse using Moshi Library for JSON Array using Kotlin Coroutines .
Code use
fun retrofitIndia(baseUrl : String) : Retrofit = Retrofit.Builder()
.client(clientIndia)
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.build()
I get issue while Parsing the data class for JSON Array . I have used same for JSON Object and it works fine but during array , it crashes
Below is the crash line
java.lang.IllegalArgumentException: Unable to create converter for java.util.ArrayList<data.india.Delta2>
I call from Globallaunch coroutine where it gets failed
Code :
GlobalScope.launch(Dispatchers.Main) {
val statsRequest = i.getStats()
try {
val response = statsRequest.await()
if(response.){
val statsResponse = response.body() //This is single object Tmdb Movie response
Log.i("stats",""+statsResponse)
}else{
Log.d("MainActivity ",response.errorBody().toString())
}
}catch (e: Exception){
Log.e("Exception",e.localizedMessage)
}
}
You should make the type just List<T>, Moshi only supports the collection interfaces, not the concrete collection classes like ArrayList<T>, LinkedList<T>, etc.. The same goes for other kinds of collections: use Set<T> instead of HashSet<T>, and Map<K, V> instead of HashMap<K, V>, etc..
I don't think the coroutines have anything with the parsing error, try following Reading Json lists using Moshi
Quick snippet will look something like:
// Single item declaration
class SingleListItem(val title: String, val number: Int)
private var listMyData = Types.newParameterizedType(MutableList::class.java, SingleListItem::class.java)
private val adapter: JsonAdapter<List<SingleListItem>> = Moshi.Builder().add(KotlinJsonAdapterFactory()).build().adapter(listMyData)
I am using the new Retrofit2 with suspending coroutines, and with GET requests everything works fine.
But I now have to implement a POST request, and just can't get it to work
I have a CURL example that looks like this:
curl -X POST -H "Content-Type: application/json;charsets: utf-8" -d '{"tx_guapptokenlist_tokenitem":{"tokenchar":"my-token-string","platform":"android"}}' https://www.example-url.com/tokens?type=56427890283537921
This works fine, and returns this response: {"errors":false,"success":true}%
So here's what my request looks like in my Api class right now:
#Headers( "Content-Type: application/json" )
#POST("/tokens?type=56427890283537921")
suspend fun sendFirebaseToken(#Body tokenRequest: RequestBody) : Call<TokenResponse>
This is my TokenResponse class:
#JsonClass(generateAdapter = true)
data class TokenResponse(
#Json(name="errors")
val errors: Boolean,
#Json(name="success")
val success: Boolean)
and the ApiClient class I'm using:
object ApiClient {
private const val BASE_URL = "https://myExampleUrl.com"
private var retrofit: Retrofit? = null
var moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val client: Retrofit?
get() {
if (retrofit == null) {
retrofit = Retrofit.Builder().baseUrl(
BASE_URL
).client(getOkHttpClient())
.addConverterFactory(MoshiConverterFactory.create())
.build()
}
return retrofit
}
fun getOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().addInterceptor(getLoggingInterceptor())
.connectTimeout(120, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS).writeTimeout(90, TimeUnit.SECONDS).build()
}
private fun getLoggingInterceptor(): HttpLoggingInterceptor {
return HttpLoggingInterceptor().setLevel(
if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.HEADERS
else HttpLoggingInterceptor.Level.NONE
)
}
}
The first odd thing I noticed: Even with the #POST annotation, if my suspend fun has no return type, I get no error, but okhttp will always send a GET request (at least the endpoint always receives a GET). Not sure if that is supposed to be like that?
Anyway: I need the return values, so I'm returning Call<TokenResponse>.
This leads me to my main problem, that I can't solve: If now I execute my code, it crashes with this log:
java.lang.IllegalArgumentException: Unable to create converter for retrofit2.Call<myapp.communication.TokenResponse>
for method TokenApi.sendToken
at retrofit2.Utils.methodError(Utils.java:52)
To try and deal with this I have used moshi-kotlin-codegen to generate the proper adapter (hence the annotations in the data class), but to no avail. The class is generated, but not used. I have tried to pass a Moshi with JsonAdapterFactory like this var moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()to my ConverterFactory but that doesn't work either.
Tried to add the generated adapter maually to moshi but that also did not work.
I've also tried returning different types in my request. The Retrofit docs state that without a converter one could only return a ResponseBody, but same result: Retrofit complains it has no converter. The same for returning Call<Void>
I feel like I'm missing something here? Who can help? Happy to provide more details, please request what's needed.
Your request function should look like this.
#Headers( "Content-Type: application/json" )
#POST("/tokens?type=56427890283537921")
suspend fun sendFirebaseToken(#Body tokenRequest: RequestBody): TokenResponse
You don't use Call<...> since you have marked it as suspend.
Think the annotation should be:
#JsonClass(generateAdapter = true)
data class TokenResponse(
#field:Json(name = "errors") val errors: Integer,
#field:Json(name = "success") val success: Boolean
)
And try to remove the suspend keyword once, which might clash with generateAdapter = true.
I've got it working now, this is what I learned:
First of all: #Dominic Fischer here is right, Call is wrong, and with everything set up correctly, there is no need to wrap the result object at all (I noticed by the way the #Headers annotation looks to be not necessary, Retrofit seems to just take care of it).
The second and biggest problem is that the client object in my ApiClient class was not used correctly. See the new version:
fun getRetrofitService(): ApiService {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.client(getOkHttpClient())
.addConverterFactory(MoshiConverterFactory.create())
.build().create(ApiService::class.java)
}
See that now the 'create()' step is added, which before I handled outside of this class. There I used my Retrofit object to create the service just like here, but I accidentally passed ApiClient::class.java. Interestingly that compiles and runs just fine, but of course this must mess up somewhere - it's unable to properly build the JSON adapters.
As a result I pulled this step into my ApiClientin order to prevent such accidents in the future.
If anybody has suggestions as to meking this question + answer more useful for future readers, please let me know!
In one of my projects, when i calculate the grand total
shippingDetails.grandTotal = 3.448873577E7
instead of 34488749.7
where grandTotal is of type Double
The above object is in my data class and i need to pass as request body for a request, but it fails since it's having scientific notation.
#POST(AppConstants.Network.CONFIRM_PENDING_SHIPPING)
fun confirmPendingShipping(
#Header("Authorization") token: String,
#Body shippingItems: RequestConfirmShipping
): Single<ResponsePendingShipping>
I'm passing it as body of post using Retrofit
Inside my app i call some remote APIs that give me a response wrapped into a useless "root" json element. I Paste you an example of json response
{
"response": {
"status": {
//here status fields, common to all responses
},
"configuration": {
//here configurations fields
}
}
}
I'm using an Android studio extension for generate kotlin data classes (JsonToKotlinClass) and i obtain four kotlin classes:
-MyResponseClass, Response, Status, Configuration
where MyResponseClass is like
data class MyResponseClass(
val response: Response
)
there's a way to avoid creation of "Response" class by parsing only it's relative json content and get a MyResponseClass look like like
data class MyResponseClass(
val status:Status,
val configuration: Configuration
)
?
From the title of your question I am assuming that you want to automatically parse the json response to the to the simpler MyResponseClass
You can achieve this by using a type adapter for gson. In your case the adapter class will look similar to the following.
class MyResponseClassAdapter : JsonDeserializer<MyResponseClass> {
override fun deserialize(jsonElement: JsonElement, p1: Type?, p2: JsonDeserializationContext?): MyResponseClass {
val content = jsonElement.asJsonObject["response"]
return Gson().fromJson(content , MyResponseClass::class.java)
}
}
Then add this adapter to a gson instance and use that gson instance with your retrofit instance.
val gson = GsonBuilder()
.registerTypeAdapter(MyResponseClass::class.java, MyResponseClassAdapter ())
.create()
Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson))
.baseUrl("{base_url}")
.client(okHttp)
.build()
Yes, when you get the object, instead of converting it straight away, convert it like this
val jsonObject = JSONObject("data")
val innerObject = jsonObject.getJSONObject("status")
and then convert inner object with gson.
This can of course be minified like this:
val object = Gson().fromJson(JSONObject("response").getJSONObject("status"), MyResponseClass::class.java)
I'm using Retrofit 2 and Moshi to read and parse JSON from an endpoint. My retrofit instance is defined like so:
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://myendpoint.com")
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create())
.build()
And I'm using the Kotlin data class to store the information in a model:
#GET("data/getlist")
fun getData(): Single<Data>
Data class:
data class Data(val Response : String,
val Message : String,
val BaseImageUrl : String)
Now, because the JSON is formatted like so, the JSON is parsed and the model is populated just fine:
{
"Response": "Success",
"Message": "Api successfully returned",
"BaseImageUrl": "https://www.endpoint.com/image/xxx.jpg",
}
This is because the objects map 1:1 with the model. So in the above example, the "Response" key mapped to the "Response" variable name in the Data class.
My question is this: what if the keys are all variable? How can you represent this in the Kotlin data class?
Sample JSON file to be parsed:
{
"RandomX": "xxxxxx",
"RandomY": "yyyyyy",
"RandomZ": "zzzzzz",
}
As #eric-cochran pointed out, you don't really need a new data class to represent this. It'd end up being a Map, and you'd use it like so:
#GET("data/getlist")
fun getVariableData(): Single<Map<String, String>>