How to parse generic key with kotlin serialization from JSON - 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)
}
}

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

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

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 to convert list of int in json to list/array of enums using Moshi?

I'm getting a list of ints (which are really enums) from the API. When I try to parse it, I get: Unable to create converter for java.util.List<MyEnum>
My adapter is currently looking like this:
#Retention(AnnotationRetention.RUNTIME)
#JsonQualifier
annotation class MyEnumListAnnotation
class MyEnumListAdapter {
#ToJson
fun toJson(#MyEnumListAnnotation myEnumList: List<MyEnum>): List<Int> {
return myEnumList.map { it.type }
}
#FromJson
#MyEnumListAnnotation
fun fromJson(typeList: List<Int>): List<MyEnum> {
return typeList.map { MyEnum.from(it) }
}
}
I'm adding this to the network client like this:
Moshi.Builder()
.add([A lot of other adapters])
.add(MyEnumListAdapter())
And I'm using the annotation like this (in the object I want to parse to):
data class InfoObject(
val id: String,
val name: String,
val email: String,
val phone: String,
#MyEnumListAnnotation
val myEnums: List<MyEnum>
)
How can I write my adapter so that this is working? Thanks for all help. :)
If you use Moshi's codegen (which you should), you only need to write adapter for your MyEnum itself.
class MyEnumAdapter {
#ToJson
fun toJson(enum: MyEnum): Int {
return enum.type
}
#FromJson
fun fromJson(type: Int): MyEnum {
return MyEnum.from(it)
}
}
Attach the adapter to your Moshi builder the way you did it in your question. Then, update your InfoObject:
#JsonClass(generateAdapter = true)
data class InfoObject(
#Json(name = "id") val id: String,
#Json(name = "name") val name: String,
#Json(name = "email") val email: String,
#Json(name = "phone") val phone: String,
#Json(name = "myEnums") val myEnums: List<MyEnum>
)
#JsonClass(generateAdapter = true) will ensure that the library will auto-create an adapter for your InfoObject, including an adapter for List<MyEnum> (the one you tried to create yourself), so you don't have to create those adapters yourself. #Json(name="...") is just a convention, you can omit it.
To integrate codegen, just add to dependencies:
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.9.3")
See https://github.com/square/moshi for more details.

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)