2

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.

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
michpohl
  • 852
  • 8
  • 30
  • Possible duplicate of [Unable to create converter for my class in Android Retrofit library](https://stackoverflow.com/questions/32367469/unable-to-create-converter-for-my-class-in-android-retrofit-library) – Martin Zeitler Sep 21 '19 at 11:15

3 Answers3

3

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.

Dominic Fischer
  • 1,695
  • 10
  • 14
  • This is right. Since it's only part of the problem I've referenced it in my answer where I explain what else is wrong. – michpohl Sep 22 '19 at 07:25
1

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.

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
  • Thanks, I will give it a try and report! In the meantime I found out that if I return a ```Response```, I get no exception anymore, but again, the endpoint states that it receives a GET request and headers and body are gone. It's possible that this is my main problem, but I guess that's a whole different question then... – michpohl Sep 21 '19 at 12:16
  • Turns out with everything properly set up, no annotations are needed. I assume the JSON objects to generate/decode here are so simple that Moshi can just figure it out. I removed them and have no problems. – michpohl Sep 22 '19 at 07:23
-1

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!

michpohl
  • 852
  • 8
  • 30