Access to Nested Json Kotlin - json

I don't know how to get data from nested Json
{
"results":[
{
"id":1,
"name":"Rick Sanchez",
"status":"Alive",
"species":"Human",
"type":"",
"gender":"Male",
Json looks like above, i want to get access to name variable.
My code:
Data class:
data class Movie(
#Json(name = "results") val results: List<MovieDetail>
)
data class MovieDetail(
#Json(name = "name") val name: String
)
ApiService:
private const val BASE_URL = "https://rickandmortyapi.com/api/"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
interface MovieApiService {
#GET("character")
suspend fun getMovies(): List<Movie>
}
object MovieApi {
val retrofitService : MovieApiService by lazy {
retrofit.create(MovieApiService::class.java)
}
}
And ViewModel:
private val _status = MutableLiveData<String>()
val status: LiveData<String> = _status
init {
getMovies()
}
private fun getMovies() {
viewModelScope.launch {
val listResult = MovieApi.retrofitService.getMovies()
_status.value = "Success: ${listResult.size} names retrieved"
}
}
For plain Json there is no problem but i don't know how to get access to this nested variables, i think that i have to use "results" variable from data class but i don't know where and how.
During running app i've got error: Expected BEGIN_ARRAY but was BEGIN_OBJECT at path $

You should change
#GET("character")
suspend fun getMovies(): List<Movie>
To:
#GET("character")
suspend fun getMovies(): Movie
You are receiving object and not list of objects

Related

Retrofit2 - generic parsing of nested json with custom deserializer

An API that I use returns data in the format:
{"status":1,
"message": "some_response_message"
"data": [{object},{object}...]
}
Depending on the specific endpoint, the {object} is different.
I am trying to create a generic deserializer to handle the responses, and am using this as my starting point: Get nested JSON object with GSON using retrofit
(At the moment I'm just trying to get the first call working with a generic deserializer so I can add more calls later on)
Here is my deserializer:
class MyDeserializer<T> : JsonDeserializer<T>{
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): T {
val data : JsonElement? = json?.asJsonObject?.get("data")
return Gson().fromJson(data, typeOfT)
}
}
However, I'm getting an error thrown when I try to use this: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $
If I set a breakpoint on the val data... line, it never gets hit, so it seems like it's failing before it gets to using the custom deserializer.
Here's how I'm using the deserializer:
#Singleton
class RetrofitCSNetwork #Inject constructor(
) : CSNetworkDataSource {
val gson: Gson = GsonBuilder()
.registerTypeAdapter(weatherResponse::class.java, MyDeserializer<weatherResponse>())
.create()
private val networkApi = Retrofit.Builder()
.baseUrl(CSBaseUrl)
.client(
OkHttpClient.Builder()
.addInterceptor(
// TODO: Decide logging logic
HttpLoggingInterceptor().apply {
setLevel(HttpLoggingInterceptor.Level.BODY)
}
)
.addInterceptor(AuthInterceptor())
.build()
)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(RetrofitCSNetworkApi::class.java)
override suspend fun getWeathers(request: GetWeatherRequest) =
networkApi.getWeathers(request)
}
My data classes:
data class weatherResponse(
#SerializedName("id") val id: Int?,
#SerializedName("city_name") val city_name: String?,
#SerializedName("image") val image: String?,
#SerializedName("daily_weather_forecasts") val forecasts: List<dailyWeatherForecasts>,
)
data class dailyWeatherForecasts(
#SerializedName("city_id") val city_id: Int?,
#SerializedName("valid_date") val valid_date: Int?,
#SerializedName("snow") val snow: Double?,
#SerializedName("max_temp") val max_temp: Double?,
)
The interface:
interface CSNetworkDataSource {
suspend fun getWeathers(request: GetWeatherRequest): Response<List<weatherResponse>>
}
Calling the function in the repo:
private val network: CSNetworkDataSource = RetrofitCSNetwork()
suspend fun getWeathers() {
val testResponse =network.getWeathers( GetWeatherRequest(null,"Colorado"))
}
Any help greatly appreciated

json converter error = moshi Expected a string but was BEGIN_OBJECT at path

How to config Moshi so that below field2 from JSON
will be converted to String "{"subfield21":"asdf","subfield22":"1234"}"
in code MyData.field2
JSON:
{
"field1":"someValue1",
"field2":{
"subfield21":"asdf",
"subfield22":"1234",
}
}
Kotlin class:
data class MyData(
val field1: String, val field2: String
)
Whan I try std Moshi config I get an exception:
moshi Expected a string but was BEGIN_OBJECT at path
Note: I'm using standalone Moshi, without retrofit.
The key is JsonReader.nextSource(). Here's an adapter factory to do the job.
#Retention(RUNTIME)
#JsonQualifier
annotation class JsonString {
object Factory {
#JsonString #FromJson fun fromJson(reader: JsonReader): String {
return reader.nextSource().use(BufferedSource::readUtf8)
}
#ToJson fun toJson(writer: JsonWriter, #JsonString value: String) {
writer.valueSink().use { sink ->
sink.writeUtf8(value)
}
}
}
}
And here's how you can use it, using the JsonQualifier annotation we made above.
#JsonClass(generateAdapter = true)
data class MyData(
val field1: String, #JsonString val field2: String
)
fun main() {
val moshi = Moshi.Builder()
.add(JsonString.Factory)
.build()
val adapter = moshi.adapter(MyData::class.java)
val decoded = adapter.fromJson(json)!!
decoded.field2 == """{"subfield21":"asdf","subfield22":"1234",}"""
}
You can't fit object inside string.
Try
data class MyData(
val field1: String, val field2: SubMyData
)
data class SubMyData(
val subfield21: String, val subfield22: String
)

How to parse generic key with kotlin serialization from JSON

I am struggling with come up with idea how to properly parse JSON like this:
{
"generic_key": { "version":1, "ttl":42 }
}
where expected kotlin class should look like this:
#Serializable
data class Config(val version: Int, val ttl: Long) {
#Transient
var key: String? = null // <== here comes generic_key
}
UPDATE
What I want to achieve is to get a kotlin class from string JSON and I don't know what key will be used as "generic_key".
UPDATE 2
Even something like this is okey for me:
#Serializable
data class ConfigWrapper(val map: Map<String, Config>)
Where there would be map with single item with key from jsonObject (e.g. generic_key) and with rest parsed with standard/generated Config.serializer.
Option 1. Define a custom deserializer, which will use plugin-generated serializer for Config class:
object ConfigDeserializer : DeserializationStrategy<Config> {
private val delegateSerializer = MapSerializer(String.serializer(), Config.serializer())
override val descriptor = delegateSerializer.descriptor
override fun deserialize(decoder: Decoder): Config {
val map = decoder.decodeSerializableValue(delegateSerializer)
val (k, v) = map.entries.first()
return v.apply { key = k }
}
}
To use it, you'll need to manually pass it to the decodeFromString method:
val result: Config = Json.decodeFromString(ConfigDeserializer, jsonString)
Option 2. Define a surrogate for Config class and a custom serializer, which will use plugin-generated serializer for ConfigSurrogate class, so that you could reject plugin-generated serializer for Config class and wire this custom serializer to Config class:
#Serializable
#SerialName("Config")
data class ConfigSurrogate(val version: Int, val ttl: Long)
object ConfigSerializer : KSerializer<Config> {
private val surrogateSerializer = ConfigSurrogate.serializer()
private val delegateSerializer = MapSerializer(String.serializer(), surrogateSerializer)
override val descriptor = delegateSerializer.descriptor
override fun deserialize(decoder: Decoder): Config {
val map = decoder.decodeSerializableValue(delegateSerializer)
val (k, v) = map.entries.first()
return Config(v.version, v.ttl).apply { key = k }
}
override fun serialize(encoder: Encoder, value: Config) {
surrogateSerializer.serialize(encoder, ConfigSurrogate(value.version, value.ttl))
}
}
#Serializable(with = ConfigSerializer::class)
data class Config(val version: Int, val ttl: Long) {
// actually, now there is no need for #Transient annotation
var key: String? = null // <== here comes generic_key
}
Now, custom serializer will be used by default:
val result: Config = Json.decodeFromString(jsonString)
Use the following data classes
data class Config(
#SerializedName("generic_key" ) var genericKey : GenericKey? = GenericKey()
)
data class GenericKey (
#SerializedName("version" ) var version : Int? = null,
#SerializedName("ttl" ) var ttl : Int? = null
)
If the key is dynamic and different, the map structure should be fine
#Serializable
data class Config(val version: Int, val ttl: Long)
val result = JsonObject(mapOf("generic_key" to Config(1, 42)))
At the end this works for me, but if there is more straight forward solution let me know.
private val jsonDecoder = Json { ignoreUnknownKeys = true }
private val jsonConfig = "...."
val result = jsonDecoder.parseToJsonElement(jsonConfig)
result.jsonObject.firstNonNullOf { (key, value) ->
config = jsonDecoder.decodeFromJsonElement<Config>(value).also {
it.key = key // this is generic_key (whatever string)
}
}

kotlin get a simple message using retrofit

Hello i am having big problems here this is my first time using retrofit and i am new to kotlin, i don't know why this piece of code is not working.
This is my retrofit client
private const val BASE_URL = "https://89a6t4gtke.execute-api.eu-west-3.amazonaws.com/Prod/"
val instance : IApi by lazy{
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
retrofit.create(IApi::class.java)
}
This is my response class
data class DefaultResponse(val message: String) {}
This is my response:
{
"message": "GET"
}
Interface
interface IApi {
#GET("hello")
fun returnHello():Call<DefaultResponse>
}
The call
toast_button.setOnClickListener{
RetrofitClient.instance
.returnHello()
.enqueue(object: Callback<DefaultResponse>{
override fun onFailure(call: retrofit2.Call<DefaultResponse>, t: Throwable) {
Toast.makeText(context,t.message + "bla",Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: retrofit2.Call<DefaultResponse>,response: Response<DefaultResponse>) {
Toast.makeText(context, "empty?",Toast.LENGTH_SHORT)
}
})
}
No toast messages show, i had an error show once when i made my api just return a string and not a json string but now there is no error as i fixed it.
You should change to your Retrofit Client as following code;
class RetrofitClient {
companion object {
fun getClient(): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://89a6t4gtke.execute-api.eu-west-3.amazonaws.com/Prod/")
.build()
}
}
}
Then add this in your activity ;
val service = RetrofitClient.getClient().create(IApi::class.java)
val call = service.returnHello()
val resp: DefaultResponse? = call.clone().execute().body()
if (resp != null) {
println("your response is -> $resp")
}

retrofit + gson deserializer: return inside array

I have api that return json:
{"countries":[{"id":1,"name":"Australia"},{"id":2,"name":"Austria"}, ... ]}
I write model class (Kotlin lang)
data class Country(val id: Int, val name: String)
And I want do request using retorift that returning List < Models.Country >, from "countries" field in json
I write next:
interface DictService {
#GET("/json/countries")
public fun countries(): Observable<List<Models.Country>>
companion object {
fun create() : DictService {
val gsonBuilder = GsonBuilder()
val listType = object : TypeToken<List<Models.Country>>(){}.type
gsonBuilder.registerTypeAdapter(listType, CountriesDeserializer)
gsonBuilder.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
val service = Retrofit.Builder()
.baseUrl("...")
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gsonBuilder.create()))
.build()
return service.create(DictService::class.java)
}
}
object CountriesDeserializer : JsonDeserializer<List<Models.Country>> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): List<Models.Country>? {
val res = ArrayList<Models.Country>()
if(json!=null) {
val countries = json.asJsonObject.get("countries")
if (countries.isJsonArray()) {
for (elem: JsonElement in countries.asJsonArray) {
res.add(Gson().fromJson(elem, Models.Country::class.java))
}
}
}
return null;
}
}
}
But I get error:
java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 2 path $
CountriesDeserializer code dont execute even!
What they want from me?
Maybe I need write my own TypeAdapterFactory?
I dont want use model class like
class Countries {
public List<Country> countries;
}
If your intention is to simplify the interface and hide the intermediate wrapper object I guess the simplest thing to do is to add an extension method to the DictService like so:
interface DictService {
#GET("/json/countries")
fun _countries(): Observable<Countries>
}
fun DictService.countries() = _countries().map { it.countries }
data class Countries(val countries: List<Country> = listOf())
Which can then be used as follows:
val countries:Observable<List<Country>> = dictService.countries()
I found the way:
object CountriesTypeFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>?): TypeAdapter<T>? {
val delegate = gson?.getDelegateAdapter(this, type)
val elementAdapter = gson?.getAdapter(JsonElement::class.java)
return object : TypeAdapter<T>() {
#Throws(IOException::class)
override fun write(outjs: JsonWriter, value: T) {
delegate?.write(outjs, value)
}
#Throws(IOException::class)
override fun read(injs: JsonReader): T {
var jsonElement = elementAdapter!!.read(injs)
if (jsonElement.isJsonObject) {
val jsonObject = jsonElement.asJsonObject
if (jsonObject.has("countries") && jsonObject.get("countries").isJsonArray) {
jsonElement = jsonObject.get("countries")
}
}
return delegate!!.fromJsonTree(jsonElement)
}
}.nullSafe()
}
}
But it is very complex decision, I think, for such problem.
Are there another one simpler way?
Another one:
I found bug in my initial code from start meassage!!!
It works fine if replace List by ArrayList!
I would use Jackson for this task.
Try this https://github.com/FasterXML/jackson-module-kotlin
val mapper = jacksonObjectMapper()
data class Country(val id: Int, val name: String)
// USAGE:
val country = mapper.readValue<Country>(jsonString)