Make JsonDeserializer global effect - json

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.

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

com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException

I am facing such an error in my application. I guess the problem is due to having char in note_title and note_desc. I couldn't find the solution. Is there anyone who can help?
navgraph
error
NoteDetailScreen
notes entity
? and other char cause error
I tried change note_title and note_desc types but didnt work.
I solved this way;
Let’s say you have a class like this:
#Parcalize
#Entity(tableName = "NOTES")
data class Notes(
#PrimaryKey(autoGenerate = true)
#ColumnInfo("note_id") #NotNull var note_id:Int,
#ColumnInfo("note_title") #NotNull var note_title: String,
#ColumnInfo("note_desc") #NotNull var note_desc: String,
#ColumnInfo("note_date") #Nullable var note_date: String?
): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readString().toString(),
parcel.readString().toString(),
parcel.readString()
) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(note_id)
parcel.writeString(note_title)
parcel.writeString(note_desc)
parcel.writeString(note_date)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Notes> {
override fun createFromParcel(parcel: Parcel): Notes {
return Notes(parcel)
}
override fun newArray(size: Int): Array<Notes?> {
return arrayOfNulls(size)
}
}
}
annotation class Parcalize
You can define the NavType like this:
class NavTypo : NavType<Notes>(isNullableAllowed = false) {
override fun get(bundle: Bundle, key: String): Notes? {
return bundle.getParcelable(key)
}
override fun parseValue(value: String): Notes {
return Gson().fromJson(value, Notes::class.java)
}
override fun put(bundle: Bundle, key: String, value: Notes) {
bundle.putParcelable(key, value)
}
}
And use it:
composable(
"note_details_page/{note_id}",
arguments = listOf(
navArgument("note_id"){
type = NavTypo()
}
)
){
val note = it.arguments?.getParcelable<Notes>("note_id")
if (note != null) {
NoteDetailScreen(note, navController)
}
}
Card(
backgroundColor = choosedColor,
modifier = Modifier
.padding(3.dp)
.sizeIn(maxHeight = 250.dp)
.combinedClickable(onClick={
val note = allNotes.value!![it]
val noteJson = Uri.encode(Gson().toJson(note))
navController.navigate("note_details_page/${noteJson}")
}

How do you serialize a list of BufferedImage in Kotlin?

I'm trying to implement a protocol where (part of it) is sending a list of small images over a socket. I'm using JSON and the images are base64 encoded.
Here's the data classes
#Serializable
sealed class CmdBase {
abstract val cmd: Command
}
#Serializable
#SerialName("CmdIdImgs")
class CmdIdImgs(
override val cmd: Command,
val id: String,
#Serializable(with = ImageListSerializer::class)
val thumbnails: List<BufferedImage>) : CmdBase()
So I added a serializer for BufferedImage
object ImageSerializer: KSerializer<BufferedImage> {
override val descriptor = PrimitiveSerialDescriptor("Image.image", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): BufferedImage {
val b64str = decoder.decodeString()
return ImageIO.read(ByteArrayInputStream(Base64.getDecoder().decode(b64str)))
}
override fun serialize(encoder: Encoder, value: BufferedImage) {
val buff = ByteArrayOutputStream()
ImageIO.write(value, "PNG", buff)
val b64str = Base64.getEncoder().encodeToString(buff.toByteArray())
encoder.encodeString(b64str)
}
}
But it's a list of BufferedImages, so I added a serializer for that
class ImageListSerializer: KSerializer<List<BufferedImage>> {
private val listSerializer = ListSerializer(ImageSerializer)
override val descriptor: SerialDescriptor = listSerializer.descriptor
override fun serialize(encoder: Encoder, value: List<BufferedImage>) {
listSerializer.serialize(encoder, value)
}
override fun deserialize(decoder: Decoder): List<BufferedImage> = with(decoder as JsonDecoder) {
decodeJsonElement().jsonArray.mapNotNull {
try {
json.decodeFromJsonElement(ImageSerializer, it)
} catch (e: SerializationException) {
e.printStackTrace()
null
}
}
}
}
And now a serializer for the whole class
object CmdIdImgsSerializer : SerializationStrategy<CmdIdImgs>, DeserializationStrategy<CmdIdImgs> {
override val descriptor = buildClassSerialDescriptor("CmdIdImgs") {
element("cmd", Command.serializer().descriptor)
element("id", String.serializer().descriptor)
element("thumbnails", ImageListSerializer().descriptor)
}
override fun serialize(encoder: Encoder, value: CmdIdImgs) {
encoder.encodeStructure(descriptor) {
encodeSerializableElement(descriptor, 0, Command.serializer(), value.cmd)
encodeSerializableElement(descriptor, 1, String.serializer(), value.id)
encodeSerializableElement(descriptor, 2, ImageListSerializer(), value.thumbnails)
}
}
override fun deserialize(decoder: Decoder): CmdIdImgs =
decoder.decodeStructure(descriptor) {
var cmd: Command = Command.FULL_TREE
var id: String = ""
var thumbnails: List<BufferedImage> = listOf()
loop# while (true) {
when (val i = decodeElementIndex(descriptor)) {
0 -> cmd = decodeSerializableElement(descriptor, i, Command.serializer())
1 -> id = decodeSerializableElement(descriptor, i, String.serializer())
2 -> thumbnails = decodeSerializableElement(descriptor, i, ImageListSerializer())
CompositeDecoder.DECODE_DONE -> break
else -> throw SerializationException("Unknown index $i")
}
}
CmdIdImgs(cmd, id, thumbnails)
}
}
But something is wrong, because I still get
Serializer has not been found for type 'BufferedImage'
on the 'val thumbnails: List<BufferedImage>' in the CmdIdImgs class
Any idea what I'm doing wrong?
Probably a lot since I'm a newbie with Kotlin :-)
Since you want to send JSON to your socket, I recommend you use de facto Jackson. If that's ok for you, then this is simpler - you only need to create one specialised serializer. Here's working code (deserializer TODO).
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.module.kotlin.KotlinModule
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.*
import javax.imageio.ImageIO
sealed class CmdBase {
abstract val cmd: String // Command
}
class CmdIdImgs(
override val cmd: String, // Command
val id: String,
val thumbnails: List<BufferedImage>,
) : CmdBase()
class BufferedImageSerializer : StdSerializer<BufferedImage>(BufferedImage::class.java) {
override fun serialize(value: BufferedImage?, jgen: JsonGenerator, provider: SerializerProvider?) {
value?.let {
val buff = ByteArrayOutputStream()
ImageIO.write(it, "PNG", buff)
val b64str = Base64.getEncoder().encodeToString(buff.toByteArray())
jgen.writeString(b64str)
}
}
}
//class BufferedImageDeserializer : StdDeserializer<BufferedImage>(BufferedImage::class.java) {
// override fun deserialize(jp: JsonParser, ctxt: DeserializationContext?): BufferedImage? {
// val node: JsonNode = jp.codec.readTree(jp)
// if (!node.isTextual) {
// node.asText()....
// }
// }
//}
val IMAGE_MODULE = SimpleModule().apply {
this.addSerializer(BufferedImage::class.java, BufferedImageSerializer())
//this.addDeserializer(BufferedImage::class.java, BufferedImageDeserializer())
}
val MAPPER = JsonMapper.builder()
.addModule(KotlinModule(strictNullChecks = true))
.addModule(IMAGE_MODULE)
.build()
fun main(args: Array<String>) {
val cmdIdImgs = CmdIdImgs("x", "1", listOf(ImageIO.read(File("/tmp/image.png"))))
println(MAPPER.writeValueAsString(cmdIdImgs))
}
Prints
{"cmd":"x","id":"1","thumbnails":["iVBORw0KGgoAAAAN....

Asynchronous call in Kotlin not working with RecycleView

I'm struggling with asynchronous calls - the app is just crashing. I want to load a JSON-file (containing 100 JSON-objects) from an URL and then send it to RecyclerView.
Here is the MainActivity-class:
class MainActivity : AppCompatActivity() {
lateinit var recyclerView: RecyclerView
lateinit var linearLayoutManager: LinearLayoutManager
private val url = [//some address here]
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = linearLayoutManager
AsyncTaskHandler().execute(url)
}
inner class AsyncTaskHandler : AsyncTask<String, String, String>() {
override fun onPreExecute() {
super.onPreExecute()
}
override fun doInBackground(vararg url: String?): String {
val text: String
val connection = URL(url[0]).openConnection() as HttpURLConnection
try {
connection.connect()
text = connection.inputStream.use { it.reader().use {reader -> reader.readText()} }
} finally {
connection.disconnect()
}
return text
}
override fun onPostExecute(result: String?) {
super.onPostExecute(result)
handleJson(result)
}
}
private fun handleJson(jsonString: String?) {
val jsonArray = JSONArray(jsonString)
var list = mutableListOf<DataSet>()
var i = 0
while (i < jsonArray.length()) {
val jsonObject = jsonArray.getJSONObject(i)
list.add(DataSet(
jsonObject.getString("title"),
jsonObject.getString("type")
))
i++
}
val adapter = Adapter(list)
recyclerView.adapter = adapter
}
}
...and ListAdapter-class:
class Adapter (private var targetData: MutableList<DataSet>): RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.element, parent, false)
return ViewHolder(v);
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = targetData[position]
holder.title?.text = item.title
holder.type?.text = item.type
}
override fun getItemCount(): Int {
return targetData.size
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var title = itemView.findViewById<TextView>(R.id.itemTitle)
var type = itemView.findViewById<TextView>(R.id.itemType)
}
What might be the problem here? Is there any better option to perform this?

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)