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)
Related
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
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)
}
}
I have my code structure like this:
File 1:
abstract class SomeClass {
abstract fun print()
companion object {
val versions = arrayOf(ClassV1::class, ClassV2::class)
}
}
#Serializable
data class ClassV1(val x: Int) : SomeClass() {
override fun print() {
println("Hello")
}
}
#Serializable
data class ClassV2(val y: String) : SomeClass() {
override fun print() {
println("World")
}
}
File 2:
fun <T : SomeClass> getSomeObject(json: String, kClass: KClass<T>): SomeClass {
return Json.decodeFromString(json)
}
fun printData(version: Int, json: String) {
val someClass: SomeClass = getSomeObject(json, SomeClass.versions[version])
someClass.print()
}
I have a json in printData that is a serialized form of some sub-class of SomeClass. I also have a version which is used to determine which class structure does the json represent. Based on the version, I want to de-serialize my json string to the appropriate sub-class of SomeClass.
Right now the getSomeObject function deserializes the json to SomeClass (which crashes, as expected). I want to know if there is a way I can deserialize it to the provided KClass.
I know I can do this like below:
val someClass = when (version) {
0 -> Json.decodeFromString<ClassV1>(json)
else -> Json.decodeFromString<ClassV2>(json)
}
But I am trying to avoid this since I can have a lot of such versions. Is there a better way possible?
It seems to me that the following is what you are looking for:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "version",
visible = false)
#JsonSubTypes(
JsonSubTypes.Type(value = ClassV1::class, name = "V1"),
JsonSubTypes.Type(value = ClassV2::class, name = "V2"))
abstract class SomeClass {
(...)
}
This basically means that your JSON would be deserialized as ClassV1 or ClassV2 based on the JSON property version:
V1 would mean that ClassV1 is the target class;
V2 would mean that ClassV2 is the target class.
You can find more information about this at the following online resources:
https://fasterxml.github.io/jackson-annotations/javadoc/2.4/com/fasterxml/jackson/annotation/JsonTypeInfo.html
https://fasterxml.github.io/jackson-annotations/javadoc/2.5/com/fasterxml/jackson/annotation/JsonSubTypes.Type.html
https://www.baeldung.com/jackson-annotations#jackson-polymorphic-type-handling-annotations
When deserialize json to Map<out Any, Any>, gson will use Double to fill the Map, even the field is int number, so I use a MapDeserializerDoubleAsIntFix to covert number to int if it is possible.
{
"person":{
"name":"jack",
"age":24,
"height":174.5
}
}
class MapDeserializerDoubleAsIntFix: JsonDeserializer<Map<out Any, Any>> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): Map<out Any, Any>? {
return deserialize(json) as Map<out Any, Any>
}
private fun deserialize(jsonElement: JsonElement): Any? {
when {
jsonElement.isJsonArray -> {
val list: MutableList<Any?> = ArrayList()
val arr = jsonElement.asJsonArray
for (anArr in arr) {
list.add(deserialize(anArr))
}
return list
}
jsonElement.isJsonObject -> {
val map: MutableMap<String, Any?> = LinkedTreeMap()
val obj = jsonElement.asJsonObject
val entitySet = obj.entrySet()
for ((key, value) in entitySet) {
map[key] = deserialize(value)
}
return map
}
jsonElement.isJsonPrimitive -> {
val prim = jsonElement.asJsonPrimitive
when {
prim.isBoolean -> {
return prim.asBoolean
}
prim.isString -> {
return prim.asString
}
prim.isNumber -> {
// Here is what i do
// use int or long if it is possible
val numStr = prim.asString
return if (numStr.contains(".")) {
prim.asDouble
} else {
val num = prim.asNumber
val numLong = num.toLong()
return if (numLong < Int.MAX_VALUE && numLong > Int.MIN_VALUE) {
numLong.toInt()
} else {
numLong
}
}
}
}
}
}
return null
}
}
object MapTypeToken: TypeToken<Map<out Any, Any>>()
private val GSON = GsonBuilder()
.registerTypeAdapter(MapTypeToken.type, MapDeserializerDoubleAsIntFix())
.create()
And when I use GSON deserialize json as map, it works.
val map: Map<out Any, Any> = GSON.fromJson(json, MapTypeToken.type)
But when the map is in a data class as a field, the MapDeserializerDoubleAsIntFix not work.
data class Test(
val person: Map<out Any, Any>
)
val test = GSON.fromJson(json, Test::class.java)
So is there any way to deserialize map or filed map ?
Now I use this to solve.
class TestJsonDeserializer: JsonDeserializer<Test> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext,
): Test {
val jsonObject = json.asJsonObject
val personElement = jsonObject.get("person")
val person = context.deserialize<Map<out Any, Any>>(personElement, MapTypeToken.type)
return Test(person)
}
}
private val GSON = GsonBuilder()
.registerTypeAdapter(MapTypeToken.type, MapDeserializerDoubleAsIntFix())
.registerTypeAdapter(Test::class.java, TestJsonDeserializer())
.create()
But if another data class has map, I have to write another JsonDeserializer and register it.
Hope there is a better way to make the MapDeserializerDoubleAsIntFix work as global.
I'm trying to replace the default Javalin JSON serializer Jackson by Kotlinx.serialization.
The documentation show how to do it with GSON serializer.
Unfortunately kotlinx serializer has a different function signature and I can't figure out how to pass arguments through.
Serialization is OK but deserialization with decodeFromString function require to be passed a type given by the mapping function as targetClass.
I'm stuck here:
val kotlinx = Json { coerceInputValues = true }
JavalinJson.toJsonMapper = object : ToJsonMapper {
override fun map(obj: Any): String = kotlinx.encodeToString(obj)
}
JavalinJson.fromJsonMapper = object : FromJsonMapper {
override fun <T> map(json: String, targetClass: Class<T>): T = kotlinx.decodeFromString(json)
}
But I get: Cannot use 'T' as reified type parameter. Use a class instead.
I also tried:
JavalinJson.fromJsonMapper = object : FromJsonMapper {
override inline fun <reified T> map(json: String, targetClass: Class<T>): T = kotlinx.decodeFromString(json)
}
But I get a warning: Override by an inline function and an error: Override by a function with reified type parameter.
I'm new to kotlin and I'm struggling understanding what's wrong with this override.
Try this one:
JavalinJson.toJsonMapper = object : ToJsonMapper {
override fun map(obj: Any): String {
val serializer = serializer(obj.javaClass)
return kotlinx.encodeToString(serializer, obj)
}
}
JavalinJson.fromJsonMapper = object : FromJsonMapper {
override fun <T> map(json: String, targetClass: Class<T>): T {
#Suppress("UNCHECKED_CAST")
val deserializer = serializer(targetClass) as KSerializer<T>
return kotlinx.decodeFromString(deserializer, json)
}
}