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>>
}