How to pass data class as parameter in kotlin extension - android

i have a data class like:
data class GetDoctorResponse(
val BrickName: String,
val Class: String,
val DoctorAddress
)
Now i want to get response, so i get it like this:
val myResponse = Gson().fromJson("response in json", Array<GetDoctorResponse>::class.java).toList()
my question is how can i create an extension which takes data and class and return me response like above.
i have tried this:
fun getResponse(data: String, it: Class<T>) : List<it> =
Gson().fromJson(data, Array<it>::class.java).toList()
but T is unresolved here, and i want to get response of any data class i passed.
val response = getResponse(data, SomeClass())

You need to Pass type also.
I didn't test it so let me know it worked or not.
inline fun <reified T : Any> getResponse(data: String) : List<T> =
Gson().fromJson(data, Array<T>::class.java).toList()

Thanks every one who helped me.
what i did is create this extension
fun <T> getResponse(data: String, model: Class<Array<T>>): List<T> =
Gson().fromJson(data, model).toList()
and call it as
val myResponse = getResponse(data, Array<GetDoctorResponse>::class.java)
and its working.

Related

Retrofit wrap response in a generic class internally

I have a sealed class like below,
sealed class ApiResult<T>(
val data: T? = null,
val errors: List<Error>? = null
) {
class Success<T>(data: T?) : ApiResult<T>(data)
class Failure<T>(
errors: List<Error>,
data: T? = null
) : ApiResult<T>(data, errors)
}
And this is my Retrofit API interface,
interface Api {
#POST("login")
suspend fun login(#Body loginRequestDto: LoginRequestDto): ApiResult<LoginResponseDto>
}
What I want to achieve is, internally wrap the LoginResponseDto in BaseResponseDto which has the success, error, and data fields. Then put them in the ApiResult class.
data class BaseResponseDto<T>(
val success: Boolean,
val errors: List<Int>,
val data: T?
)
In this case, LoginResponseDto is my data class. So, Retrofit should internally wrap the LoginResponseDto in BaseResponseDto then I will create the ApiResult response in my custom call adapter. How can I tell Retrofit to internally wrap this? I don't want to include the BaseResponseDto in the API interface every time.
After spending the whole day, I could achieve it. I had to create a custom converter. Here it is,
class ApiConverter private constructor(
private val gson: Gson
) : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
val baseResponseType = TypeToken.get(BaseResponseDto::class.java).type
val finalType = TypeToken.getParameterized(baseResponseType, type)
return Converter<ResponseBody, Any> { value ->
val baseResponse: BaseResponseDto<*> =
gson.fromJson(value.charStream(), finalType.type)
baseResponse
}
}
companion object {
fun create(gson: Gson) = ApiConverter(gson)
}
}
Now I don't have to specify the BaseResponseDto every time in the API interface for retrofit.
Great question!
If you want to implement your own solution you would need a custom Retrofit CallAdapter.
My recommendation would be Sandwich an Open source Library which does the exact same thing.
If you want to implement your own soultion here's one way
Modeling Retrofit Responses With Sealed Classes and Coroutines

Creating json from generic strings

I'm trying to do something really simple (at least, should be IMO): create a function that receives a string containing some json and turns that into a Gson object. I've created the following function in my class:
class EasyJson(val url: String, private val responseDataClass: Class<*>) {
var text: String
var json: Gson
init {
text = getJsonFromUrl(URL(url)) //another function does this and is working fine
json = stringToJson(text, responseDataClass::class.java) as Gson
}
private fun <T> stringToJson(data: String, model: Class<T>): T {
return Gson().fromJson(data, model)
}
And here is the calling class:
class CallingClass() {
val url="https://api"
init {
test()
}
data class ApiResponse(
val count: Number,
val next: Number?,
val previous: Number?
)
private fun test(){
val json = EasyJson(url, ApiResponse::class.java)
}
}
However, when I do this I get the following exception:
java.lang.UnsupportedOperationException: Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?
How can I use a generic data class as a parameter for Gson here?
The issue here is most likely the following line:
stringToJson(text, responseDataClass::class.java)
This will use the class of the class, which is always Class<Class>. You should just call your stringToJson function with responseDataClass (without ::class.java):
stringToJson(text, responseDataClass)
Additionally it looks like you treat the stringToJson result incorrectly. The result is an instance of T (actually T? because Gson.fromJson can return null). However, you are casting it to Gson.
The correct code would probably look like this:
class EasyJson<T>(val url: String, private val responseDataClass: Class<T>) {
var text: String
var json: T?
init {
text = getJsonFromUrl(URL(url)) //another function does this and is working fine
json = stringToJson(text, responseDataClass)
}
private fun <T> stringToJson(data: String, model: Class<T>): T? {
return Gson().fromJson(data, model)
}
}
(Though unless your code contains more logic in EasyJson, it might make more sense to move this JSON fetching and parsing to a function instead of having a dedicated class for this.)

Moshi create json with dynamic inner data class

I have moshi with retrofit and I want to send dynamic inner data object to backend.
Lets say I need to patch some data on my endpoint:
#PATCH("/animals/{animalId}/attributes")
suspend fun updateAnimal(#Path("animalId") animalId: String, #Body request: MyPetsRequest)
But my AnimalAttributes inner object is dynamic.
#JsonClass(generateAdapter = true)
#Parcelize
data class MyPetsRequest(
#Json(name = "name") val name: String,
#Json(name = "attributes") val attributes: AnimalAttributes
) : Parcelable
interface AnimalAttributes : Parcelable // or sealed class possible when share same base fields
#JsonClass(generateAdapter = true)
#Parcelize
data class DogAttributes(
#Json(name = "bark") val bark: Boolean,
#Json(name = "bite") val bite: Boolean,
) : AnimalAttributes
#JsonClass(generateAdapter = true)
#Parcelize
data class CatAttributes(
#Json(name = "weight") val weight: Int,
) : AnimalAttributes
I tried create a custom adapter for Moshi, but when I use #ToJson inner object attributes data are escaped (so complete object is send as one parameter).
class AnimalAttributesAdapter {
#ToJson
fun toJson(value: AnimalAttributes): String {
val moshi = Moshi.Builder().build()
return when (value) {
is DogAttributes -> moshi.adapter(DogAttributes::class.java).toJson(value)
is CatAttributes -> moshi.adapter(CatAttributes::class.java).toJson(value)
else -> throw UnsupportedOperationException("Unknown type to serialize object to json: $value")
}
}
/**
* Not supported, this object is used only one way to update data on server.
*/
#FromJson
fun fromJson(value: String): AnimalAttributes = throw UnsupportedOperationException()
}
Moshi.Builder().add(AnimalAttributesAdapter())
Result in this case looks like:
{"name":"my pet name","attributes":"{\"bark\":\"true\", \"bite\":\"false\"}"}
When I try to use PolymorphicJsonAdapterFactory it added next parameter between attributes which is not acceptable for my use case.
PolymorphicJsonAdapterFactory.of(AnimalAttributes::class.java, "animal")
.withSubtype(DogAttributes::class.java, "dog")
.withSubtype(CatAttributes::class.java, "cat")
Result in this case added attribute animal: {"name":"my pet name","attributes":{"animal":"dog","bark":true, "bite":false}}
I don't need to deserialize object from json to data class. I only need create one way serialization to json.

Is there a way to make extension function to data class type only?

My problem is that i want one extension function that works only on data classes.
for example lets say I have multiple data classes.
data class Person(
val name: String,
val age: Int
)
data class Car(
val color: Color,
val brand: String
)
and so on.
Now i want to create an extension function that is only extended for the data classes, not like this were i have to create extension function for each class.
for example my extension functions:
fun Person.convertToJson() : String {
return Gson().toJson(this)
}
fun Car.convertToJson() : String {
return Gson().toJson(this)
}
I want only one function that does the magic, also i don't want to use generic since it will be available to all objects. This is the generic function example:
fun <T> T.convertToJson() : String {
return Gson().toJson(this)
}
I want something equivalent to the generic function that will only work for data classes type.
There is no feature in the language to define an extension function on all data classes. if you can modify your data classes to implement an interface then you can use a marker interface for this as
/* define a marker interface and have all your data classes implement it */
interface Jsonable
data class Person(val name: String, val age: Int): Jsonable
data class Car(val color: Color, val brand: String): Jsonable
Now you can define the extension function on the interface as
fun Jsonable.convertToJson(): String = Gson().toJson(this)
and you can use it as
Person("A", 50).convertToJson()
I think you couldn't make an extension function to the data class type but you can overcome this by making an interface that your data class can extend and then make an extension for any class that implements the defined interface
interface IHuman
data class Person(val name: String, val age: Int): IHuman
data class Student(val name: String, val schoolName: String): IHuman
inline fun <reified T: IHuman> T.doSomething(){
println("Which one call me: ${T::class.java.simpleName}")
}
fun main(){
Person(name = "Mohamed", age = 28).doSomething()
Student(name = "Ahmed", schoolName = "MySchool").doSomething()
}
I hope below code will help to solve your problem without any Interface
ExtensionFunction:
inline fun <reified T : Any> T.objectToString(): String = Gson().toJson(this, T::class.java)
inline fun <reified T : Any> String.stringToObject(): T = Gson().fromJson(this, T::class.java)
Call to make:
Object.objectToString() // This will return string
String.stringToObject<Object>() // This will return Object

Moshi cannot parse nullable

Hello) Hope you can help me.
Using kotlin (Retrofit2 + moshi) i getting data from "https://api.spacexdata.com/v3/launches" and parsing it.
All is going fine (i getting attributes like: flight_number, mission_name), but some attributes have "null", like "mission_patch" - there are 111 objects. 109 of them have data at "mission_patch", 2 objects dont have it ("mission_patch":null).
My problem: moshi cannot parse correctly attribute which contains null.
if i using:
data class SpaceXProperty(
val flight_number: Int,
val mission_name: String,
val mission_patch: String)
i getting error: "Failure: Required value "mission_patch" missing at $[1]" - OK i changed data class to next:
data class SpaceXProperty(
val flight_number: Int,
val mission_name: String,
val mission_patch: String?)
with this i getting data, but every object have mission_patch=null. This is uncorrect, bc only 2 objects have mission_patch=null, not all.
Help me please. im new at kotlin, what i doing wrong?
My retrofit service:
private const val BASE_URL = "https://api.spacexdata.com/v3/"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
//.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
interface SpaceXApiService {
#GET("launches")
suspend fun getProperties():List<SpaceXProperty>
}
object SpaceXApi{
val retrofitservice :SpaceXApiService by lazy {
retrofit.create(SpaceXApiService::class.java)
}
}
mission_patch is not in the root object like flight_number etc. It's nested inside links. So your model should match. Try this:
data class SpaceXProperty(
val flight_number: Int,
val mission_name: String,
val links: Links) {
data class Links(val mission_patch: String?)
}

Categories

Resources