Retrofit2 - generic parsing of nested json with custom deserializer - json

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

Related

Access to Nested Json Kotlin

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

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

Deserialize JSON array with different values type with kotlinx.serialization library

I'm trying to deserialize following String:
val stringJson = "{\"decomposed\":[\", \",{\"id\":4944372,\"name\":\"Johny\",\"various\":false,\"composer\":false,\"genres\":[]}]}"
Deserialization works fine with following code
#Serializable
data class Artist(
val decomposed: JsonArray
)
fun main() {
val jsonString = "{\"decomposed\":[\", \",{\"id\":4944372,\"name\":\"Johny\",\"various\":false,\"composer\":false,\"genres\":[]}]}"
println(Json.decodeFromString<Artist>(jsonString))
}
But I want to do something like
#Serializable
class Decomposed {
#Serializable
class DecomposedClassValue(val value: DecomposedClass)
#Serializable
class StringValue(val value: String)
}
#Serializable
data class DecomposedClass(
val id: Long? = null,
val name: String? = null,
val various: Boolean? = null,
val composer: Boolean? = null,
val genres: JsonArray? = null
)
#Serializable
data class Artist(
val decomposed: List<Decomposed>
)
fun main() {
val jsonString = "{\"decomposed\":[\", \",{\"id\":4944372,\"name\":\"Johny\",\"various\":false,\"composer\":false,\"genres\":[]}]}"
println(Json.decodeFromString<Artist>(jsonString))
}
But kotlinx.serialization expectedly fails with JsonDecodingException: Unexpected JSON token at offset 15: Expected '{, kind: CLASS'
And I can't figure out how can I rewrite my Decomposed so deserialization work. Can you please help me out?
What you are trying to do is called polymorphic deserialization.
It requires target classes of deserialization to have a common superclass (preferrably sealed):
#Serializable
data class Artist(
val decomposed: List<Decomposed>
)
#Serializable
sealed class Decomposed
#Serializable
class StringValue(val value: String) : Decomposed() //Can't add superclass to String, so we have to create a wrapper class which we could make extend Decomposed
#Serializable
data class DecomposedClass(
val id: Long? = null,
val name: String? = null,
val various: Boolean? = null,
val composer: Boolean? = null,
val genres: JsonArray? = null
) : Decomposed() //DecomposedClassValue is redundant, we may extend DecomposedClass from Decomposed directly
This will allow you to deserialize JSON of the following format:
val jsonString = "{\"decomposed\":[{\"type\":\"StringValue\", \"value\":\",\"}, {\"type\":\"DecomposedClass\", \"id\":4944372,\"name\":\"Johny\",\"various\":false,\"composer\":false,\"genres\":[]}]}"
Since there is no class descriminator in original JSON, serialization library can't determine the actual serializer which should be used to deserialize Kotlin class. You will have to write custom JsonContentPolymorphicSerializer and wire it to Decomposed class; also you have to write custom serializer for StringValue class, as it is represented in JSON as a String, not a JSONObject with a value field of String type:
object DecomposedSerializer : JsonContentPolymorphicSerializer<Decomposed>(Decomposed::class) {
override fun selectDeserializer(element: JsonElement) = when {
element is JsonPrimitive -> StringValue.serializer()
else -> DecomposedClass.serializer()
}
}
object StringValueSerializer : KSerializer<StringValue> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("StringValue")
override fun deserialize(decoder: Decoder): StringValue {
require(decoder is JsonDecoder)
val element = decoder.decodeJsonElement()
return StringValue(element.jsonPrimitive.content)
}
override fun serialize(encoder: Encoder, value: StringValue) {
encoder.encodeString(value.value)
}
}
#Serializable(with = DecomposedSerializer::class)
sealed class Decomposed
#Serializable(with = StringValueSerializer::class)
class StringValue(val value: String) : Decomposed()
This will allow you to deserialize JSON of original format.

How do you register the KotlinModule to the AWS lambda Jackson Object Mapper?

I'm using Kotlin to write an AWS Lambda. I have a Kotlin data class
class MessageObject(
val id: String,
val name: String,
val otherId: String
)
This data class is used as the input to the required interface implementation
class Handler : RequestHandler<MessageObject, Output> {
...
override fun handleRequest(msg: MessageObject, ctx: Context) {
...
}
}
When I test this lambda in the aws console, and pass it a proper JSON message, I get this:
An error occurred during JSON parsing: java.lang.RuntimeException
java.lang.RuntimeException: An error occurred during JSON parsing
Caused by: java.io.UncheckedIOException:
com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of 'com.mycode.MessageObject'(no Creators, like default construct, exist):
cannot deserialize from Object value (no delegate- or property-based Creator)
I'm almost certain this is fixed by saying:
ObjectMapper().registerModule(KotlinModule())
but in the world of AWS Lambda how do I edit the object mapper provided by AWS?
If you haven't gotten it to work with KotlinModule, since the problem you're having is that Jackson requires a default empty constructor and you currently don't have one. You could just change your MessageObject as follows and it should work:
data class MessageObject(
var id: String = "",
var name: String = "",
var otherId: String = ""
)
I created this repo with a fully functional kotlin lambda template using the Serverless Framework. Have a look for some other tidbits you might need: https://github.com/crafton/sls-aws-lambda-kotlin-gradlekt
You cannot use data class with provided RequestHandler<I, O> unfortunately, because you need register the kotlin module for your jackson mapper in order to work with data classes. But you can write you own RequestHandler, which will like this one.
Here's the code:
interface MyRequestStreamHandler<I : Any, O : Any?> : RequestStreamHandler {
val inputType: Class<I>
fun handleRequest(input: I, context: Context): O?
override fun handleRequest(inputStream: InputStream, outputStream: OutputStream, context: Context) {
handleRequest(inputStream.readJson(inputType), context).writeJsonNullable(outputStream)
}
interface MessageObjectRequestHandler : MyRequestStreamHandler< MessageObject, Output> {
override val inputType: Class<MessageObject >
get() = MessageObject::class.java
}
}
And jackson util:
private val objectMapper = jacksonObjectMapper()
.configure(JsonParser.Feature.ALLOW_COMMENTS, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerKotlinModule()
private val writer: ObjectWriter = objectMapper.writer()
fun <T : Any> readJson(clazz: Class<T>, stream: InputStream): T =
objectMapper.readValue(stream, clazz)
fun <T : Any> InputStream.readJson(clazz: Class<T>): T =
readJson(clazz, this)
fun Any?.writeJsonNullable(outputStream: OutputStream) {
if (this != null) writer.writeValue(outputStream, this)
}
Now, you can keep your MessageObject class to be data class, and your handler will look something like:
class LambdaMain : MessageObjectRequestHandler {
override fun handleRequest(input: MessageObject, context: Context): Output {
//...
}
}

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)