GSON - Unable to unparse an Abstract Class - json

I have an abstract class "Elem" with a bunch of children (TXT, IMG, EDT ...). They all have contructors.
I need to parse in json an Object contaning a list of children of an abstract class
abstract class Elem(
var content : String,
var condition : String = ""
){
open fun instantiate(){
}
}
class TXT(content: String) : Elem(content) {
override fun instantiate() {
//Some work
}
}
class BTN(content: String, private val additional : String) : Elem(content) {
override fun instantiate() {
//Some work
}
}
...
EDIT :
I tried to used the AbstractElementAdapter, as shown here
Here the new code to parse a JSON
val e = MyObject(/*Some stuff,*/listOf(TXT("Hello"), IMG("world.png"))))
val gsonBuilder = GsonBuilder()
gsonBuilder.registerTypeAdapter(EtapElem::class.java, JsonSerializer<EtapElem>{
src, _, context ->
val result = JsonObject()
result.add("type", JsonPrimitive(src.javaClass.simpleName))
result.add("elem", context.serialize(src, src.javaClass))
return#JsonSerializer result
})
val jsonPretty: String = gsonBuilder .setPrettyPrinting().create().toJson(e)
The json looks fine
{
//Some stuff,
"elems": [
{
"type": "TXT",
"elem": {
"content": "Hello?",
"condition": ""
}
},
{
"type": "IMG",
"elem": {
"content": "world.png",
"condition": ""
}
}
]
}
Now the read :
val gsonBuilder = GsonBuilder()
gsonBuilder.registerTypeAdapter(EtapElem::class.java, JsonDeserializer<EtapElem>{
json, _, context ->
val jsonObject = json.asJsonObject
val type = jsonObject["type"].asString
val element = jsonObject["elem"]
return#JsonDeserializer try {
context.deserialize(element, Class.forName("com.package.path.elem.$type"))
} catch (cnfe: ClassNotFoundException) {
throw JsonParseException("Unknown element type: $type", cnfe)
}
})
val outtype = object : TypeToken<MyObject>() {}.type
val s : Scenario = gsonBuilder.create().fromJson(jsonFileString, outtype)
I have an exception thrown in read :
java.lang.ClassCastException: class com.package.path.elem.TXT cannot be cast to class java.lang.Void (com.package.path.elem.TXT is in unnamed module of loader 'app'; java.lang.Void is in module java.base of loader 'bootstrap')

Related

Kotlinx Deserialization Object or List of Object to List of Object

I am currently implementing an API client with Ktor. The API I am requesting does not return a consistent JSON format.
for Example:
sometimes the JSON looks like this:
{
"description": {
"lang": "en",
"value": "an English description..."
},
...
}
and sometimes like this:
{
"description": [
{
"lang": "en",
"value": "an English description..."
},
{
"lang": "fr",
"value": "a French description..."
}
],
...
}
Now my Question:
How can I implement a Custom Kotlinx Deserializer to Decode an Object of T or a List<T> to a List<T>
My classes look like this:
#Serializable
class ResourceResponse(
#SerialName("description")
val descriptions: List<Description>
) {
#Serializable
data class Description(
#SerialName("value")
val value: String,
#SerialName("lang")
val language: String,
)
}
I want that a Json with only one Description-Object will be deserialized to a List with one Object and not specifically for the description, but in general for classes.
I've found nothing really helpful in the Web.
One solution is to first deserialize it to JsonElement, introspect and then decide how to deserialize it further into ResourceResponse:
fun decode(s: String): ResourceResponse {
val json = Json.parseToJsonElement(s).jsonObject
return when (val desc = json["description"]) {
is JsonArray -> Json.decodeFromJsonElement(json)
is JsonObject -> {
val json2 = json.toMutableMap()
json2["description"] = JsonArray(listOf(desc))
Json.decodeFromJsonElement(JsonObject(json2))
}
else -> throw IllegalArgumentException("Invalid value for \"description\": $desc")
}
}
This solution is definitely not ideal. It may be potentially less performant as we need to deserialize the whole tree into the tree of JsonElement objects only to transform it to the final types (although, maybe the library does this internally anyway). It works only for json and it is tricky to use this solution if ResourceResponse is somewhere deep into the data structure.
You can use a JsonContentPolymorphicSerializer to choose a deserializer based on the form of the JSON.
This one should work:
#Suppress("UNCHECKED_CAST")
class DescriptionsSerializer : JsonContentPolymorphicSerializer<List<ResourceResponse.Description>>(
List::class as KClass<List<ResourceResponse.Description>>
) {
// Here we check the form of the JSON we are decoding, and choose
// the serializer accordingly
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out List<ResourceResponse.Description>> {
return if (element is JsonArray)
ListSerializer(ResourceResponse.Description.serializer())
else
SingleDescriptionAsList()
}
class SingleDescriptionAsList : KSerializer<List<ResourceResponse.Description>> {
override val descriptor: SerialDescriptor
get() = ResourceResponse.Description.serializer().descriptor
override fun deserialize(decoder: Decoder): List<ResourceResponse.Description> {
return listOf(ResourceResponse.Description.serializer().deserialize(decoder))
}
override fun serialize(encoder: Encoder, value: List<ResourceResponse.Description>) {
throw Exception("Not in use")
}
}
}
You must also amend your original class to tell it to use this serializer:
#Serializable
class ResourceResponse(
#SerialName("description")
#Serializable(with = DescriptionsSerializer::class) val descriptions: List<Description>
) {
#Serializable
data class Description(
#SerialName("value")
val value: String,
#SerialName("lang")
val language: String,
)
}
Then you will be able to decode JSON objects with the single key "descriptions" using the ResourceResponse serializer.
For avoidance of doubt, if there are other keys in the JSON (it's not entirely clear from the question) then those should also be written into ResourceResponse definition.
After my research, I have now come up with a solution. For this you need a wrapper class. (here GenericResponse). I hope I can help others who have the same problem.
This is the Wrapper-Class
#Serializable(with = ListOrObjectSerializer::class)
class GenericResponse<T>(
val data: List<T> = emptyList()
) {
private var _isNothing : Boolean = false
val isNothing: Boolean
get() {
return this._isNothing
}
companion object {
fun <T> nothing(): GenericResponse<T> {
val o = GenericResponse(emptyList<T>())
o._isNothing = true
return o
}
}
}
And the Serializer looks like:
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.*
class ListOrObjectSerializer<T : Any>(private val tSerializer: KSerializer<T>): KSerializer<GenericResponse<T>> {
override val descriptor: SerialDescriptor
get() = tSerializer.descriptor
override fun deserialize(decoder: Decoder): GenericResponse<T> {
val input = decoder as JsonDecoder
val jsonObj = input.decodeJsonElement()
return when(jsonObj) {
is JsonObject -> GenericResponse(listOf(Json.decodeFromJsonElement(tSerializer, jsonObj)))
is JsonArray -> GenericResponse(Json.decodeFromJsonElement(ListSerializer(tSerializer), jsonObj))
else -> return GenericResponse.nothing()
}
}
override fun serialize(encoder: Encoder, value: GenericResponse<T>) {
throw IllegalAccessError("serialize not supported")
}
}
My Data-Class look now like:
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
#Serializable
class ResourceResponse(
#SerialName("description")
val descriptions: GenericResponse<Description>? = null,
) {
#Serializable
data class Description(
#SerialName("value")
val value: String? = null,
#SerialName("lang")
val language: String? = null,
)
}
data class ResourceResponse(
#SerializedName("description") val descriptions: List<Description>,
)
data class Description(
#SerializedName("value") val value: String,
#SerializedName("lang") val language: String,
)
it should be like that

I tried to parse a JSON file on my app but even if there is no error, my app doesn't run on the emulator

The problem might be that the app can't reach the json file I tried to parse it to.No error pops up in the run or build but it stops at"11/23 23:14:38: Launching 'app' on Device 4.Install successfully finished in 325 ms."Below is my code:
MainActivity
class MainActivity : AppCompatActivity() {
#SuppressLint("SuspiciousIndentation", "NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val usersList: ArrayList<UserModelClass> = ArrayList()
try {
val obj = JSONObject(getJSONFromAssets()!!)
val usersArray = obj.getJSONArray("owner")
for (i in 0 until usersArray.length()) {
val user = usersArray.getJSONObject(i)
val login = user.getInt("id")
val name = user.getString("name")
val url = user.getString("url")
val followers_url = user.getString("followers")
val starred_url = user.getString("stars")
val userDetails =
UserModelClass(login, name, url, followers_url , starred_url)
// add the details in the list
usersList.add(userDetails)
}
} catch (e: JSONException) {
//exception
e.printStackTrace()
}
val recyclerView = findViewById<RecyclerView>(R.id.rvUsersList)
val adapter = UserAdapter(this, usersList)
recyclerView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
recyclerView.adapter = adapter
adapter.notifyDataSetChanged()
}
private fun getJSONFromAssets(): String? {
var json: String? = null
val charset: Charset = Charsets.UTF_8
try {
val myUsersJSONFile = assets.open("JSON.json")
val size = myUsersJSONFile.available()
val buffer = ByteArray(size)
myUsersJSONFile.read(buffer)
myUsersJSONFile.close()
json = String(buffer, charset)
} catch (ex: IOException) {
ex.printStackTrace()
return null
}
return json
}
}
UserAdapter
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
class UserAdapter( val context: Context, val items: ArrayList<UserModelClass>) :
RecyclerView.Adapter<UserViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
return UserViewHolder(
LayoutInflater.from(context).inflate(
R.layout.item_user,
parent,
false
)
)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int {
return items.size
}
}
UserViewHolder
class UserViewHolder(view: View) : RecyclerView.ViewHolder(view) {
// Holds the TextView that will add each item to
var tvId : TextView
var tvName : TextView
var tvURL : TextView
var tvFollowers : TextView
var tvStars : TextView
init {
tvId = view.findViewById(R.id.tvId)
tvName = view.findViewById(R.id.tvName)
tvURL = view.findViewById(R.id.tvURL)
tvFollowers = view.findViewById(R.id.tvFollowers)
tvStars = view.findViewById(R.id.tvStars)
}
fun bind(item: UserModelClass) {
tvId.text = item.login.toString()
tvName.text = item.node_id
tvURL.text = item.avatar_url
tvFollowers.text = item.followers_url.toString()
tvStars.text = item.starred_url.toString()
}
}
UserModelClass
class UserModelClass(
val login: Int,
val node_id: String,
val avatar_url: String,
val followers_url: String,
val starred_url: String
)
Json.json
I don't need all the information from the json, I selected some of them
{
"total_count": 357602,
"incomplete_results": false,
"items": [
{
"id": 23096959,
"node_id": "...",
"name": "...",
"full_name": "...",
"private": false,
"owner": {
"login": "...",
"id": 4314092,
"node_id": "...",
"avatar_url": "https://...",
"url": "https://api.github.com/users/golang",
"followers_url": "https://api.github.com/users/golang/followers",
"following_url": "https://api.github.com/users/golang/following{/other_user}",
"starred_url": "https://api.github.com/users/golang/starred{/owner}{/repo}",
},
Actually it seems you are parsing it wrong. In your json, items is the array and owner is an object inside items array item.
You cannot access owner directly. Also you should give exact keys.
Like "followers_url" not just "followers"
Try the below code.
try {
val obj = JSONObject(getJSONFromAssets()!!)
val itemsArray = obj.getJSONArray("items")
for (i in 0 until itemsArray.length()) {
val user = itemsArray.getJSONObject(i)
val name = user.getString("name") // Getting name from item object
val owner = user.getJSONObject("owner") //Getting owner object from item object
//And below you need to get items from owner object not the user object.
val login = owner.getInt("id")
val url = owner.getString("url")
val followers_url = owner.getString("followers_url")
val starred_url = owner.getString("starred_url")
val userDetails =
UserModelClass(login, name, url, followers_url , starred_url)
// add the details in the list
usersList.add(userDetails)
}
} catch (e: JSONException) {
//exception
e.printStackTrace()
}

Moshi error: #JsonClass can't be applied to [class]. must not be sealed

Imagine this data sample
"meta_data": [
{
"id": 40097,
"key": "_wcf_frm_created",
"value": ""
},
{
"id": 40098,
"key": "_wcf_custom_degin_checkbox",
"value": ""
},
{
"id": 40099,
"key": "_wcf_frm_data",
"value": {
"1": {
"1": "",
"2": "",
"3": "chk_box"
}
}
},
{
"id": 40119,
"key": "_vendor_select",
"value": "6484"
},
{
"id": 40120,
"key": "_vendor_percentage",
"value": "1"
},
{
"id": 40121,
"key": "_vendor_pro_cat",
"value": "Accessories"
}
]
the Value in Meta_data can have multiple types. In the generator I used shown that the data type should be created like this.
sealed class Value {
class StringMapMapValue(val value: Map<String, Map<String, String>>) : Value()
class StringValue(val value: String) : Value()
}
With Moshi, I understand you have to add #JsonClass(generateAdapter = true) on top of the data class. Thus I have something like this
#JsonClass(generateAdapter = true)
data class MetaDatum (
val id: Long,
val key: String,
val value: Value
)
#JsonClass(generateAdapter = true)
sealed class Value {
class StringMapMapValue(val value: Map<String, Map<String, String>>) : Value()
class StringValue(val value: String) : Value()
}
I would like to note that the full json is much bigger than this. However, this is the only issue I have. I had some Enum issues as well, but those can be replaced with String
The error I received is
error: #JsonClass can't be applied to net......Activity.Value: must not be sealed
public static abstract class Value
Thus my question is, how do i decode the json with multiple enum types.
Ill add this here, In xCode(swift) this was how i was manage to do it.
enum Value: Codable {
case string(String)
case stringMapMap([String: [String: String]])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode([String: [String: String]].self) {
self = .stringMapMap(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(Value.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Value"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
case .stringMapMap(let x):
try container.encode(x)
}
}
}
Calling the data
fun retrieveMenu(sku: Int, SSLAuth: String)
{
doAsync {
val client = OkHttpClient().newBuilder()
.build()
val formBody: RequestBody = FormBody.Builder()
.build()
val request: Request = Request.Builder()
.url("https://carteapp.net/..................")
.method("Get", formBody)
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val gist =
gistJsonAdapter.fromJson(response.body!!.source())
println(gist)
}
}
}
private val moshi = Moshi.Builder().build()
private val gistJsonAdapter = moshi.adapter(BarcodeScannerActivity.WcProductCall::class.java)
#JsonClass(generateAdapter = true)
data class WcProductCall (
val id: Long,
...........
val metaData: List<MetaDatum>,
...
)
This is a bit tricky as Moshi won't allow sealed/abstract classes, my idea would be to use the DTO pattern (This is a pattern for using a Data Transfer Object to pass as much data in a single network request as possible)
Create a data class (your DTO) that contains both / all the data
#JsonClass(generateAdapter = true)
data class MetaDatumDTO (
val id: Long,
val key: String,
val value1: Value1DTO?,
val value2: Value2DTO?
)
#JsonClass(generateAdapter = true)
data class Value1DTO(val value: Map<String, Map<String, String>>)
#JsonClass(generateAdapter = true)
data class Value2DTO(val value: String)
And then inside your repository / model, when you retrieve the data, use a mapper function to map the data to the data classes you want to use
data class MetaDatum(
val id: Long,
val key: String,
val value: Value?
)
sealed class Value {
data class Value1(val value: Map<String, Map<String, String>>) : Value()
data class Value2(val value: String) : Value()
}
fun MetaDatumDto.mapFromNetworkRequest(): MetaDatum {
return MetaDatum(
id = id,
key = key,
value = getValueFromDTO()
)
}
fun MetaDatumDto.getValueFromDTO(): Value? {
return (value1 as? Value1) ?: (value2 as? Value2)
}

Skipping serialization of null values depending on the type

I started working with moshi a couple of weeks ago, so maybe I am missing something trivial, but I spent already quite a bit of time trying to fix this without success, so here is my question.
Having the following reproducible code:
fun main() {
val moshi = Moshi.Builder().add(OptionalAdapter).build()
val objectToSerialize = DummyObject()
val json = moshi.adapter(DummyObject::class.java).serializeNulls().toJson(objectToSerialize)
println(json)
}
#JsonClass(generateAdapter = true)
data class DummyObject(val value: Int=123, val someNullable: String? = null,
val someNotPresent: Optional<String> = Optional.NotPresent,
val somePresent: Optional<String> = Optional.Present("aaaa"))
class OptionalAdapter<T>(private val valueAdapter: JsonAdapter<T>) : JsonAdapter<Optional<T>>() {
#Suppress("UNCHECKED_CAST")
override fun fromJson(reader: JsonReader) = Optional.Present(valueAdapter.fromJson(reader) as T)
override fun toJson(writer: JsonWriter, value: Optional<T>?) {
when (value) {
is Optional.NotPresent -> writer.nullValue()
is Optional.Present -> valueAdapter.serializeNulls().toJson(writer, value.value)
}
}
companion object Factory : JsonAdapter.Factory {
override fun create(type: Type, annotations: Set<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
return if (Types.getRawType(type) == Optional::class.java && annotations.isEmpty()) {
val valueType = if(type is ParameterizedType) {
type.actualTypeArguments.get(0)
} else {
//Should not happen
throw IllegalArgumentException()
}
return OptionalAdapter(moshi.adapter<Any>(valueType).nullSafe())
} else {
null
}
}
}
}
sealed class Optional<out T> {
val provided get() = this !is NotPresent
abstract val value: T
object NotPresent : Optional<Nothing>() {
// have the IDE raise an error if the user knows a type is missing but still tries to access a value
#Deprecated(
"Cannot access a missing value",
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("TODO(\"value is missing\")")
)
override val value: Nothing
get() = error("cannot access provided field")
}
data class Present<out T>(override val value: T) : Optional<T>()
}
I would like to serialize as {"value":123,"someNullable":null,"somePresent":"aaaa"} instead of {"value":123,"someNullable":null,"someNotPresent":null,"somePresent":"aaaa"}, which is what is doing now.
Basically, I want to skip the serialization in case the type is Optional.NotPresent. Any suggestion?
The solution I ended up with:
override fun toJson(writer: JsonWriter, value: Optional<T>?) {
when (value) {
is Optional.NotPresent -> {
val wasSerializeNulls = writer.serializeNulls
writer.serializeNulls = false
try {
writer.nullValue()
} finally {
writer.serializeNulls = wasSerializeNulls
}
}
is Optional.Present -> valueAdapter.serializeNulls().toJson(writer, value.value)
}
}

class com.google.gson.internal.LinkedTreeMap cannot be cast to class Partner

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
fun main() {
val jsonString: String = """{
"jsonrpc": "2.0",
"id": null,
"result": [
{
"id": 1,
"name": "Lekhnath Rijal"
},
{
"id": 2,
"name": "Administrator"
}
]
}"""
val body1 = Gson().fromJson<RpcResult<List<Partner>>>(jsonString, object: TypeToken<RpcResult<List<Partner>>>(){}.type)
println(body1.result[0].name) // prints Lekhnath Rijal // - As expected
val body2 = fromJson<RpcResult<List<Partner>>>(jsonString)
println(body2.result[0].name) // throws Exception as stated below after this code snippet
}
fun <T> fromJson(json: String?): T {
return Gson().fromJson<T>(json, object: TypeToken<T>(){}.type)
}
data class RpcResult<T>(
val jsonrpc: String,
val id: Int?,
val result: T
)
data class Partner(
val id: Int,
val name: String
)
Exception: java.lang.ClassCastException: class com.google.gson.internal.LinkedTreeMap cannot be cast to class RpcResult
while converting json string to data class object without using function it works as expected but executing same code from helper function does not work and instead throws an exception mentioned above. What am I missing here?
It is due to type erasure in runtime. In Kotlin you can solve this issue by making your function inline with reified type:
Change your function from:
fun <T> fromJson(json: String?): T {
return Gson().fromJson<T>(json, object: TypeToken<T>(){}.type)
}
To:
inline fun <reified T> fromJson(json: String?): T {
return Gson().fromJson<T>(json, object: TypeToken<T>(){}.type)
}
For further reading check this out: https://kotlinlang.org/docs/reference/inline-functions.html