Home > database >  Parsing JSON Strings with moshi and retrofit2
Parsing JSON Strings with moshi and retrofit2

Time:06-02

The API I am using responds with a JSON object nested inside a list, like so:

[
    {
        "name": "Seattle",
        "lat": 47.6038321,
        "lon": -122.3300624,
        "country": "US",
        "state": "Washington"
    }
]

I'd like to parse the JSON with Moshi into the following class:

package com.example.weatherapp.entity

// the main JSON response
data class LocationWeather(val name: String, val lat: Float, val lon: Float)

My API service file is as follows.

package com.example.weatherapp.network

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.GET
import com.example.weatherapp.entity.LocationWeather
import retrofit2.http.Query

private const val BASE_URL = "http://api.openweathermap.org/geo/1.0/"


private val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()

private val retrofit = Retrofit.Builder()
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .baseUrl(BASE_URL)
    .build()


interface WeatherAPIService {
    @GET("direct?")
    fun getWeatherFromAPI(@Query("q") loc: String,
                          @Query("limit") lim: Int,
                          @Query("appid") key: String): Call<LocationWeather>
}

object WeatherApi {
    val retrofitService : WeatherAPIService by lazy {
        retrofit.create(WeatherAPIService::class.java)
    }
}

My ViewModel, which actually connects to the API, is as follows:

package com.example.weatherapp.overview


import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.weatherapp.entity.LocationWeather
import com.example.weatherapp.network.WeatherApi
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

class OverviewViewModel : ViewModel() {

    // the mutable backing property
    private var _response = MutableLiveData<String>()

    // the immutable exposed property
    val response: LiveData<String>
        get() = _response



    fun getWeather(location: String) {
        WeatherApi.retrofitService.getWeatherFromAPI(location, 1, "API_KEY_REDACTED").enqueue(
            object: Callback<LocationWeather> {
                override fun onResponse(call: Call<LocationWeather>, response: Response<LocationWeather>) {
                    _response.value = response.body()?.name
                }

                override fun onFailure(call: Call<LocationWeather>, t: Throwable) {
                    _response.value = "Failure: "   t.message
                }
            }
        )
    }

}

The fragment associated with this ViewModel just observes the response LiveData and renders the value to a TextView. The value rendered is "Failure: Expected BEGIN_OBJECT but was BEGIN_ARRAY at path $". I believe the problem is that, since the JSON is nested inside a list, the values are not being stored properly in the LocationWeather data class. The code otherwise compiles and the emulator runs. Attempting to use this same code with a different API that is NOT nested in a list works exactly as intended (provided I update the parameter names in LocationWeather). When forced to parse the JSON above, however, I am at a loss. I read the moshi docs and several others posts but I am not entirely certain how to implement their solutions using adapters or the Types.newParameterizedType() method.

How can I parse this JSON with moshi and retrofit2?

CodePudding user response:

The sample response data is array.

[
    {
        "name": "Seattle",
        "lat": 47.6038321,
        "lon": -122.3300624,
        "country": "US",
        "state": "Washington"
    }
]

[] <- This is array.

Change the response type. This is example.

interface WeatherAPIService {
    @GET("direct?")
    fun getWeatherFromAPI(@Query("q") loc: String,
                          @Query("limit") lim: Int,
                          @Query("appid") key: String): Call<List<LocationWeather>>
}

CodePudding user response:

Your API is returning a list of LocationWeather objects, but your code is trying to fetch a single LocationWeather object. So, it throwing the mentioned exception.

Update your code to fix the issue:

 fun getWeather(location: String) {
        WeatherApi.retrofitService.getWeatherFromAPI(location, 1, "API_KEY_REDACTED").enqueue(
            object: Callback<List<LocationWeather>> {
                override fun onResponse(call: Call<List<LocationWeather>>, response: Response<List<LocationWeather>>) {
                    // here I'm trying to access the first element of the list using 0th index.
                    _response.value = response.body()[0]?.name
                }

                override fun onFailure(call: Call<LocationWeather>, t: Throwable) {
                    _response.value = "Failure: "   t.message
                }
            }
        )
    }

You have to also update the return type of the method in Interface:

interface WeatherAPIService {
    @GET("direct?")
    fun getWeatherFromAPI(@Query("q") loc: String,
                          @Query("limit") lim: Int,
                          @Query("appid") key: String): Call<List<LocationWeather>>
}
  • Related